普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月22日技术

用 Vue3 + Coze API 打造冰球运动员 AI 生成器:从图片上传到风格化输出

作者 ohyeah
2025年12月22日 14:56

本文将带你从零构建一个基于 Vue3 和 Coze 工作流的趣味 AI 应用——“宠物变冰球运动员”生成器。通过上传一张宠物照片,结合用户自定义的队服编号、颜色、位置等参数,即可生成一张风格化的冰球运动员形象图。


一、项目背景与目标

在 AI 能力逐渐普及的今天,越来越多开发者尝试将大模型能力集成进自己的 Web 应用中。本项目的目标是打造一个轻量、有趣、可分享的前端应用:

  • 用户上传宠物照片;
  • 自定义冰球队服(编号、颜色)、场上位置(守门员/前锋/后卫)、持杆手(左/右)以及艺术风格(写实、乐高、国漫等);
  • 后端调用 Coze 平台的工作流 API,完成图像生成;
  • 最终返回生成结果并展示。

这类“趣味换脸/换装”类应用非常适合社交传播,比如冰球协会举办活动时,鼓励用户上传自家宠物照片生成“冰球明星”,再分享至朋友圈,既有趣又具传播性。


二、技术栈与核心流程

技术选型

  • 前端框架:Vue 3(<script setup> + Composition API)
  • 状态管理ref 响应式变量
  • HTTP 请求:原生 fetch
  • AI 能力平台Coze(提供工作流和文件上传 API)
  • 环境变量import.meta.env.VITE_PAT_TOKEN(用于安全存储 PAT Token)

核心业务流程

  1. 图片预览:用户选择图片后,立即在前端显示预览(使用 FileReader + Base64);
  2. 上传图片:将图片通过 FormData 上传至 Coze 文件服务,获取 file_id
  3. 调用工作流:携带 file_id 与用户配置参数,调用 Coze 工作流 API;
  4. 展示结果:解析返回的图片 URL 并渲染。

三、代码详解:从模板到逻辑

1. 模板结构(Template)

<template>
  <div class="container">
    <div class="input">
      <!-- 图片上传与预览 -->
      <div class="file-input">
        <img :src="imgPreview" alt="" v-if="imgPreview">
        <input type="file"
         ref="uploadImage" 
         accept="image/*"
         @change="updataImageData"
         required>
      </div>

      <!-- 配置项:队服、位置、风格等 -->
      <div class="settings">
        <div class="selection">
          <label>队服编号:</label>
          <input type="number" v-model="uniform_number">
        </div>
        <div class="selection">
          <label>队服颜色:</label>
          <select v-model="uniform_color">
            <option value="红"></option>
            <option value="蓝"></option>
            <!-- 其他颜色... -->
          </select>
        </div>
      </div>

      <div class="settings">
        <div class="selection">
          <label>位置</label>
          <select v-model="position">
            <option value="0">守门员</option>
            <option value="1">前锋</option>
            <option value="2">后卫</option>
          </select>
        </div>
        <div class="selection">
          <label>持杆:</label>
          <select v-model="shooting_hand">
            <option value="0">左手</option>
            <option value="1">右手</option>
          </select>
        </div>
        <div class="selection">
          <label>风格:</label>
          <select v-model="style">
            <option value="写实">写实</option>
            <option value="乐高">乐高</option>
            <!-- 多种艺术风格... -->
          </select>
        </div>
      </div>
       
      <!-- 生成按钮 -->
      <div class="generate">
        <button @click="generate">生成</button>
      </div>
    </div>

    <!-- 输出区域 -->
    <div class="output">
      <div class="generated">
        <img :src="imgUrl" alt="" v-if="imgUrl">
        <div v-if="status">{{ status }}</div>
      </div>  
    </div>
  </div>
</template>

关键点

  • 使用 v-if 控制预览图和结果图的显示;
  • accept="image/*" 限制仅可选择图片文件;
  • 所有配置项均通过 v-model 双向绑定到响应式变量。

2. 响应式状态声明(Script Setup)

import { ref, onMounted } from 'vue'

const imgPreview = ref('') // 本地预览图(Base64)
const uniform_number = ref(10)
const uniform_color = ref('红')
const position = ref(0)
const shooting_hand = ref('左手') // 注意:实际传给后端的是 0/1,此处为显示用
const style = ref('写实')

// 生成状态与结果
const status = ref('')
const imgUrl = ref('')

// Coze API 配置
const patToken = import.meta.env.VITE_PAT_TOKEN
const uploadUrl = 'https://api.coze.cn/v1/files/upload'
const workflowUrl = 'https://api.coze.cn/v1/workflow/run'
const workflow_id = '7567272503635771427'

🔒 安全提示VITE_PAT_TOKEN 是 Personal Access Token,绝不能硬编码在代码中!应通过 .env 文件注入,并确保 .gitignore 中排除该文件。


3. 图片预览功能:用户体验的关键

const uploadImage = ref(null)

onMounted(() => {
  console.log(uploadImage.value) // 挂载后指向 input DOM
})
// 状态 null -> input DOM  ref也可以用来绑定DOM元素

const updataImageData = () => {
  const input = uploadImage.value
  if (!input.files || input.files.length === 0) return
  // 文件对象 html新特性
  const file = input.files[0]
  const reader = new FileReader() // 
  reader.readAsDataURL(file)
  // readAsDateURL 返回Base64编码的DataURL 可直接用于<img src>
  reader.onload = (e) => {
    imgPreview.value = e.target.result // // 响应式状态 当拿到图片文件后 立马赋给imgPreview的value 那么此时template中img的src就会接收这个状态 从而响应展示图片
  }
}

🌟 为什么需要预览?

  • 用户上传的图片可能较大,上传需时间;
  • 立即显示预览能提升交互反馈感;
  • FileReader.readAsDataURL() 将图片转为 Base64,无需网络请求即可显示。

4. 上传图片到 Coze:获取 file_id

const uploadFile = async () => {
  const formData = new FormData()
  const input = uploadImage.value
  if (!input.files || input.files.length <= 0) return

  formData.append('file', input.files[0])

  const res = await fetch(uploadUrl, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${patToken}`
    },
    body: formData
  })

  const ret = await res.json()
  console.log(ret)
  if (ret.code !== 0) {
    status.value = ret.msg
    return
    // 当code为0时 表示没有错误 那么这里进行判断 当不为0时 返回错误信息给status.value
  }

  return ret.data.id // 关键:返回 file_id 供后续工作流使用
}

⚠️ 常见错误排查

  • 若返回 {"code":700012006,"msg":"cannot get access token from Authorization header"},说明 patToken 未正确设置或格式错误;
  • 确保请求头为 'Authorization': 'Bearer xxx',注意大小写和空格。

5. 调用 Coze 工作流:生成 AI 图像

const generate = async () => {
  status.value = '图片上传中...'
  const file_id = await uploadFile()
  if (!file_id) return

  status.value = '图片上传成功,正在生成中...'

  const parameters = {
    picture: JSON.stringify({ file_id }), // 注意:需 stringify
    style: style.value,
    uniform_color: uniform_color.value,
    uniform_number: uniform_number.value,
    position: position.value,
    shooting_hand: shooting_hand.value
  }

  const res = await fetch(workflowUrl, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${patToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      workflow_id,
      parameters
    })
  })

  const ret = await res.json()
  if (ret.code !== 0) {
    status.value = ret.msg
    return
  }

  const data = JSON.parse(ret.data) // 注意:Coze 返回的是字符串化的 JSON
  imgUrl.value = data.data
  status.value = ''
}

重要细节

  • picture 字段必须是 JSON.stringify({ file_id }),因为 Coze 工作流节点可能期望字符串输入;
  • ret.data 是字符串,需再次 JSON.parse 才能得到真正的结果对象;
  • 若遇到 {"code":4000,"msg":"The requested API endpoint GET /v1/workflow/run does not exist..."},说明你用了 GET 方法,但该接口只支持 POST

四、样式与布局(Scoped CSS)

<style scoped>
.container {
  display: flex;
  flex-direction: row;
  height: 100vh;
}

.input {
  display: flex;
  flex-direction: column;
  min-width: 330px;
}

.generated {
  width: 400px;
  height: 400px;
  border: solid 1px black;
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>

✨ 使用 scoped 确保样式隔离,避免污染全局;弹性布局实现左右两栏(配置区 + 结果区)。


五、总结与延伸

本项目完整展示了如何将 前端交互AI 工作流 结合:

  • 利用 Vue3 的响应式系统管理状态;
  • 通过 FileReader 实现即时预览;
  • 使用 fetch + FormData 安全上传文件;
  • 调用 Coze API 实现“上传 → 生成 → 展示”闭环。

最后提醒:

  • 务必保护好你的 PAT Token
  • 遵守 Coze 的 API 调用频率限制,如果无法响应,可以尝试更换你的Coze API;
  • 测试不同风格下的生成效果,优化用户体验。

通过这个小而美的项目,你不仅能掌握 Vue3 的实战技巧,还能深入理解如何将 AI 能力无缝集成到 Web 应用中。快去试试吧,让你的宠物穿上冰球队服,成为下一个 AI 冰球明星!🏒🐶

顶层元素问题:popover vs. dialog

2025年12月22日 14:21

原文:Top layer troubles: popover vs. dialog 作者:Stephanie Eckles 日期:2025年12月1日 翻译:田八

来源:前端周刊

你是否曾尝试通过设置 z-index: 9999 解决元素层级问题?如果是,那你其实是在与一个基础的CSS概念 ——层叠上下文—— 斗争。

层叠上下文定义了元素在第三维度(即“z轴”)上的排列顺序。你可以把z轴想象成视口中层叠上下文根节点与用户(即通过浏览器视口观察的你)之间的DOM元素的层级。

image.png

一个元素只能在同一层叠上下文中重新调整层级。虽然 z-index 是实现这一点的工具,但失败往往源于层叠上下文的变化。这种变化可能通过多种方式发生,例如使用固定定位(fixed)、粘性定位(sticky)元素,或是将绝对定位(absolute)/相对定位(relative)与 z-index 结合使用等,MDN 上列出了更多原因

现代网页设计有一个“顶层”特性,它保证使其位于所有其他层叠上下文的最顶层。它覆盖整个视口,不过顶层中的元素实际可见尺寸可能更小。

将元素提升到顶层,可使其摆脱原本所在的任何层叠上下文。

虽然顶层直接解决了一个与CSS相关的问题,但目前还没有属性可用于将元素提升到顶层。取而代之的是,某些元素和特定条件可以访问顶层,例如通过 <div> 标签显示的原生对话框 showModal() 和被指定为 Popover 的元素。

Popover API是一项新推出的 HTML 功能,它允许你声明式的创建非模态覆盖元素。通过使用 Popover API 用来摆脱任何层叠上下文,这是它的一个理想特性。然而,在急于选择这种原生能力之前,需要注意一个潜在的问题。

场景设定

想象一下,在2025年的网络世界:你的网页应用包含一个通过“Toast”消息显示通知的服务。你知道的,就是那些通常出现在角落或其他不太可能遮挡其他用户界面(UI)位置的弹出消息。

通常,这些Toast通知通常用于实时提醒,比如保存成功,或者表单提交失败等错误提示。它们有时有时间限制,或者包含如关闭按钮这样的关闭机制。有时它们还包含额外操作,例如“重试”选项,用于重新提交失败的工作流。

既然您的应用紧跟时代潮流,你最近决定将Toast升级为使用Popover API。这样你就可以将Toast组件放置在应用的任何结构中,而无需为了解决层叠上下文问题而采用一些变通方法。毕竟,Toast必须显示在所有其他元素之上,因此通过 Popover 实现顶层访问是明智之举!

你发布了改进版本,并为自己的工作感到自豪。

发布的当周晚些时候,你收到了一份紧急错误报告。不是普通的错误报告,而是一个可访问性违规报告。

Dialog vs. popover

你的应用很新潮,你之前也升级使用了原生HTML对话框。那是一次很棒的升级,因为你用原生 Web 功能取代了对 JavaScript 的依赖。这也是你兴奋地将Toast也升级为使用Popover的另一个原因。

那么,错误是什么呢?一位键盘用户正在使用一个包含对话框的工作流程,对话框打开期间,后台进程触发了一个弹出式通知。该通知提示存在错误,需要用户进行交互。

当这位键盘用户试图将焦点切换到Toast上时,出现了错误。他们虽然在视觉上能看到Toast显示在对话框背景之上,但焦点无法成功进入Toast,而是意外地跳到了浏览器UI上。

你可以在这个CodePen示例中亲自体验这个错误,使用Tab键,你会发现你永远无法访问到Toast。你也可以尝试使用屏幕阅读器,会发现虚拟光标也无法进入Toast。

CodePen

如果你能够点击弹出框,可能会觉得至少点击操作是可行的。但很快我们就会发现,事情并非如此。

为什么Toast弹出框无法访问

虽然顶层可以超越标准的层叠上下文,但顶层中的元素仍然受分层顺序的影响。最近添加到顶层的元素会显示在之前添加的顶层元素之上。这就是为什么Toast在视觉上会显示在对话框背景之上。

如果弹出框在视觉上可用,那为什么通过键盘或屏幕阅读器的虚拟光标却无法访问呢?

原因在于弹出框与 模态 对话框之间存在竞争关系。当通过 showModal()方法启动原生HTML对话框时,对话框外部的页面会变为 惰性状态惰性状态 是一种必要的可访问性行为,它会隔离对话框内容,并阻止通过Tab键和虚拟光标访问背景页面。

这个错误是由于Toast弹出框是背景页面DOM的一部分。这意味着由于它位于对话框DOM边界之外,所以也变成了惰性状态。

但是,由于顶层顺序的原因,因为它是在对话框打开后创建的,所以在视觉上,它被放置在对话框的顶部,这一点可能会让你感到困惑。

如果你以为点击弹出框就能关闭它,实际上并非如此,尽管弹出框确实会消失。真正发生的情况是,你触发了弹出框的 轻触关闭 行为。这意味着它关闭是因为你实际上点击了它的边界之外,因为对话框捕获了点击操作。

所以,虽然弹出框被关闭了,但“重试”按钮实际上并没有被点击,这意味着任何关联的事件监听器都不会被触发。

即使你创建了一个自动化测试来专门检查当对话框打开时Toast的提醒功能,该自动化测试仍可能出现误报,因为它触发了对Toast按钮的编程式点击。这种伪点击错误地绕过了由于对话框导致页面变为惰性状态所引发的问题。

重新获得弹出框访问权限

解决方案有两个方面:

  1. 将弹出框(popover)在DOM中实际放置在对话框(dialog)内部。
  2. 确保使用 popover="manual",以防止对话框内的点击操作过早触发弹出框的轻触关闭。

完成这两步后,弹出框现在既在视觉上可用,又可以通过任何方式完全交互。

Codepan

经验教训与额外考虑

我们了解到,如果你的网站或应用有可能同时显示弹出框和对话框,并且它们有独立的时间线,那么你需要想出一种在对话框内启动弹出框的机制。

或者,您可以选择在对话框关闭之前禁用后台页面弹出窗口。但如果通知需要及时交互,或者对话框内容有可能触发 Toast 提示,则此方法可能并不理想。

除了可见性和交互性之外,您可能还需要考虑另一个问题:弹出窗口是否需要在对话框关闭后继续保持打开状态。也就是说,即使对话框关闭,弹出窗口也需要保持打开状态,例如继续等待用户执行操作。

虽然我非常支持使用原生平台功能,而且我认为弹出框(popover)尤其出色,但有时冲突是无法完全避免的。事实上,您可能已经遇到过类似的问题,即模态对话框的惰性行为。因此,本文的主要目的是提醒您,如果同时显示背景弹出框和模态对话框,可能会出现问题,因此不要完全放弃之前自定义的弹出框架构。

如果这个问题目前或将来会影响到你的工作,请关注这个HTML问题,其中正在讨论解决方案。

关于斯蒂芬妮·埃克尔斯

Stephanie Eckles 是 Adobe Spectrum CSS 的高级设计工程师,也是 CSSWG 的成员,同时还是 ModernCSS.dev 的作者。Steph 拥有超过 15 年的 Web 开发经验,她乐于以作家、研讨会讲师和会议演讲者的身份分享这些经验。她致力于倡导无障碍设计、可扩展 CSS 和 Web 标准。业余时间,她是两个女儿的妈妈,喜欢烘焙和水彩画。

博客:ModernCSS.dev Mastodon:@5t3ph

译者注:

  1. popover:弹出框指的是轻提示的弹出式框,没有过多的交互逻辑
  2. dialog:对话框指的是带有交互逻辑的弹出框,例如存在确认和取消按钮,输入框等

这两个都是新特性,具体内容可参考MDN

React 的新时代已经到来:你需要知道的一切

2025年12月22日 14:20

原文: The next era of React has arrived: Here's what you need to know

翻译: 嘿嘿

来源:前端周刊

构建异步 UI 向来都是一件非常困难的事情。导航操作将内容隐藏在加载指示器之后,搜索框在响应无序到达时会产生竞态条件,表单提交则需要手动管理每一个加载状态标志和错误信息。每个异步操作都迫使你手动进行协调。

image.png

这不是一个性能问题,而是一个协调问题。现在,React 的原语声明式地解决了它。

对于开发团队而言,这标志着我们构建方式的一次根本性转变。React 不再需要每位开发者在每个组件中重新发明异步处理逻辑,而是提供了标准化的原语来自动处理协调。这意味着更少的 Bug、更一致的用户体验,以及更少的调试竞态条件的时间。

React 的异步协调原语

image.png

在 React Conf 2025 上,来自 React 团队的 Ricky Hanlon 演示的 Async React 示例,展示了未来的可能性:一个包含搜索、标签页和状态变更的课程浏览应用,在快速网络下感觉即时,在慢速网络下也能保持流畅。UI 更新自动协调,不会闪烁。

这不是一个新库,而是 React 19 的协调 API 与 React 18 的并发特性 的结合。它们共同构成了 React 团队称之为  “异步 React(Async React)”  的完整系统,通过可组合的原语来构建响应式的异步应用程序:

  • useTransition:跟踪待处理的异步工作。
  • useOptimistic:在状态变更期间提供即时反馈(乐观更新)。
  • Suspense:声明式地处理加载边界。
  • useDeferredValue:在快速更新期间保持稳定的用户体验。
  • use() :使数据获取(和上下文读取)变得声明式。

理解这些部分如何协同工作是关键,它使我们能从命令式的异步代码转向声明式的协调。

问题:手动的异步协调

在这些原语出现之前,开发者必须手动编排每一个异步操作。表单提交需要显式的加载和错误状态:

function SubmitButton() {
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState(null);

    async function handleSubmit() {
        setIsLoading(true);
        setError(null);
        try {
            await submitToServer();
            setIsLoading(false);
        } catch (e) {
            setError(e.message);
            setIsLoading(false);
        }
    }

    return (
        <div>
            <button onClick={handleSubmit} disabled={isLoading}>
                {isLoading ? '提交中...' : '提交'}
            </button>
            {error && <div>错误:{error}</div>}
        </div>
    );
}

数据获取也遵循类似的命令式模式,使用 useEffect

function UserProfile({ userId }) {
    const [user, setUser] = useState(null);
    const [isLoading, setIsLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        setIsLoading(true);
        setError(null);
        fetchUser(userId)
            .then(data => {
                setUser(data);
                setIsLoading(false);
            })
            .catch(e => {
                setError(e.message);
                setIsLoading(false);
            });
    }, [userId]);

    if (isLoading) return <div>加载中...</div>;
    if (error) return <div>错误:{error}</div>;

    return <div>{user.name}</div>;
}

每个异步操作都重复这个模式:跟踪加载状态、处理错误、协调状态更新。当这种模式扩展到几十个组件时,就会导致不一致的加载状态、被遗忘的错误处理,以及难以调试的微妙竞态条件。

原语详解

Actions 自动跟踪异步工作

React 19 引入了 Actions 来声明式地处理异步协调。将一个异步函数包装在 startTransition 中,可以让 React 跟踪整个操作:

const [isPending, startTransition] = useTransition();

function submitAction() {
    startTransition(async () => {
        await submitToServer();
    });
}

isPending 标志在 Promise 解决之前一直为 true。React 会自动处理此状态,并且在 Transition 中抛出的错误会冒泡到错误边界(Error Boundary),而不是在分散的 try/catch 块中处理(你仍需自己处理预期的错误,如验证失败)。

React 将在 Transition 中调用的任何函数被称为 “Action”。命名约定很重要:为函数添加 “Action” 后缀表示它们运行在 Transition 中(例如,submitActiondeleteAction)。

以下是使用 Actions 重写的相同按钮:

function SubmitButton() {
    const [isPending, startTransition] = useTransition();

    function submitAction() {
        startTransition(async () => {
            await submitToServer();
        });
    }

    return (
        <button onClick={submitAction} disabled={isPending}>
            {isPending ? '提交中...' : '提交'}
        </button>
    );
}

另一种选择是使用 React 19 的 <form> 组件,它可以通过接受一个 action 属性并将其自动包装在 Transition 中来为你处理:

async function submitAction(formData) {
    await submitToServer(formData);
}

<form action={submitAction}>
    <input name='username' />
    <button>提交</button>
</form>;

与手动 Action 一样,错误仍会冒泡到错误边界。当你希望在 UI 中反映表单状态时,React 19 提供了表单实用程序:useFormStatus 让子组件可以访问表单的待处理状态,而 useActionState 则允许你根据 Action 的结果更新组件状态(例如显示验证错误或“点赞”计数)。

相同的模式也适用于按钮、输入框和标签页等可复用组件。你的设计组件可以暴露 actionsubmitAction 或 changeAction 等 Action 属性,并在内部使用 Transitions 来管理待处理状态和其他异步行为。我们稍后将回到这个模式。

乐观更新提供即时反馈

Actions 提供了待处理状态,但“待处理”并不总是正确的反馈。当你点击复选框来标记任务完成时,它应该立即切换。等待服务器的响应很可能会破坏流程导致竟态问题。

useOptimistic() 在 Transitions 内部工作,用于在异步 Action 在后台运行时显示即时更新:

function CompleteButton({ complete }) {
    const [optimisticComplete, setOptimisticComplete] = useOptimistic(complete);
    const [isPending, startTransition] = useTransition();

    function completeAction() {
        startTransition(async () => {
            setOptimisticComplete(!optimisticComplete);
            await updateCompletion(!optimisticComplete);
        });
    }

    return (
        <button onClick={completeAction} className={isPending ? 'opacity-50' : ''}>
            {optimisticComplete ? <CheckIcon /> : <div></div>}
        </button>
    );
}

复选框会立即切换。如果请求成功,服务器状态将与乐观更新匹配。如果失败,服务器状态保持旧值,因此复选框会自动恢复其原始状态。

与 useState(它会延迟 Transition 内部的更新)不同,useOptimistic 会立即更新。Transition 边界定义了乐观状态的生命周期:它仅在异步 Action 处于待处理状态时持续存在,一旦 Transition 完成,就会自动“落定”到事实来源(props 或服务器状态)。(注:简单说就是当 transition 为 pending 时 optimisticComplete 为 startTransition 中设定的值,而一旦 transition 完成即 pending 为 false 时,optimisticComplete 会放弃 startTransition 的状态而使用传入的值及为例子中的 complete)

Suspense 声明式地协调加载状态

乐观更新处理了状态变更,但初始数据加载呢?useEffect 模式迫使我们手动管理 isLoading 状态。Suspense 通过允许我们声明式地定义加载边界来解决这个问题。我们需要控制显示什么后备 UI 以及如何分割加载,因此应用的独立部分可以并行加载。

Suspense 与“支持 Suspense”的数据源协同工作:异步服务器组件、使用 use() API 读取的 Promise(我们接下来会介绍),以及像 TanStack Query 这样的库(它提供了用于缓存和去重的 useSuspenseQuery)。

以下是 Suspense 如何协调多个独立数据流:

function App() {
    return (
        <div>
            <h1>仪表板</h1>
            <Suspense fallback={<ProfileSkeleton />}>
                <UserProfile />
            </Suspense>
            <Suspense fallback={<PostsSkeleton />}>
                <UserPosts />
            </Suspense>
        </div>
    );
}

每个组件都可以通过自己的后备方案独立挂起。父组件通过 Suspense 边界处理加载状态,而不是协调多个 useEffect 调用。但有个问题:当你触发导致组件重新获取数据的更新时(如切换标签页或导航),加载后备方案会再次显示,隐藏你已经看到的内容,并产生突兀的加载状态。

结合 Transition 与 Suspense

将 Transition 与 Suspense 结合可以解决这个问题,它告诉 React 保持现有内容可见,而不是立即再次显示后备方案。以下是一个针对标签页切换的适配示例:

function App() {
    const [tab, setTab] = useState('profile');
    const [isPending, startTransition] = useTransition();

    function handleTabChange(newTab) {
        startTransition(() => setTab(newTab));
    }

    return (
        <div>
            <nav>
                <button onClick={() => handleTabChange('profile')}>个人资料</button>
                <button onClick={() => handleTabChange('posts')}>帖子</button>
            </nav>
            <Suspense fallback={<LoadingSkeleton />}>
                <div style={{ opacity: isPending ? 0.7 : 1 }}>{tab === 'profile' ? <UserProfile /> : <UserPosts />}</div>
            </Suspense>
        </div>
    );
}

现在,加载后备方案仅在初始加载时显示。当你切换标签页时,Transition 会在新数据在后台加载时保持当前内容可见。不透明度样式使其变暗,以表示更新正在进行。一旦就绪,React 会自动无缝地换入新内容。没有突兀的加载状态,没有卡顿。

关键在于:Transitions 会“暂缓”UI 更新,直到异步工作完成,从而防止 Suspense 边界在导航期间回退到后备状态。像 Next.js 这样的框架使用此功能在新路由加载时保持页面可见。

use() 直接读取异步数据

早些时候,我们看到了 Suspense 如何与“支持 Suspense”的数据源协同工作。use() API 就是这样的数据源之一:它为数据获取提供了 useEffect 的替代方案,允许你在渲染期间读取 Promise。

以下是用 Suspense 和 use() 重写的最初的 useEffect 示例:

function UserProfile({ userId }) {
    const user = use(fetchUser(userId));
    return <div>{user.name}</div>;
}

function App({ userId }) {
    return (
        <ErrorBoundary fallback={<div>加载用户时出错</div>}>
            <Suspense fallback={<div>加载中...</div>}>
                <UserProfile userId={userId} />
            </Suspense>
        </ErrorBoundary>
    );
}

组件在读取 Promise 时挂起,触发最近的 Suspense 边界,然后在 Promise 解决时带着数据重新渲染。错误被错误边界捕获。与 Hooks 不同,use() 可以条件调用。

一个注意事项:Promise 需要被缓存。否则,每次渲染都会重新创建它。在实践中,你可以使用像 Next.js 这样处理缓存和去重的框架。

延迟值防止 UI 过载

Actions 和 Suspense 处理离散的操作:点击、提交、导航。但快速输入(如搜索)需要不同的方法,因为你希望输入框即使在结果加载时也能保持响应。

一种方法可以是设计一个 SearchInput 组件,通过内部乐观状态保持输入响应,并在 Transition 中调用 changeAction,这样父组件只需传递 value 和 changeAction

当你没有设计组件时,useDeferredValue() 提供了类似的拆分效果。虽然你可以用它来延迟昂贵的 CPU 计算(性能),但此处的目标是稳定的用户体验。

结合 Suspense、use() 和ErrorBoundary,我们可以获得完整的搜索体验:

function SearchApp() {
    const [query, setQuery] = useState('');
    const deferredQuery = useDeferredValue(query);
    const isStale = query !== deferredQuery;

    return (
        <div>
            <input value={query} onChange={e => setQuery(e.target.value)} />
            <ErrorBoundary fallback={<div>加载结果时出错</div>}>
                <Suspense fallback={<div>搜索中...</div>}>
                    <div style={{ opacity: isStale ? 0.5 : 1 }}>
                        <SearchResults query={deferredQuery} />
                    </div>
                </Suspense>
            </ErrorBoundary>
        </div>
    );
}

function SearchResults({ query }) {
    if (!query) return <div>开始输入以搜索</div>;
    const results = use(fetchSearchResults(query));
    return (
        <div>
            {results.map(r => (
                <div key={r.id}>{r.name}</div>
            ))}
        </div>
    );
}

Suspense 后备方案仅在初始加载时显示。在后续搜索期间,useDeferredValue 会在新结果于后台加载时保持旧结果可见(通过 isStale 降低不透明度)。错误边界隔离了失败,即使数据请求失败,搜索输入也能保持功能正常。

综合应用:Async React 示例

到目前为止,我们分别了解了每个原语。Async React 示例 展示了当一个框架将它们整合到路由、数据获取和设计系统中时会发生什么:

gif of async react demo

尝试切换网络速度以查看 UI 如何适应:在快速连接下即时,在慢速连接下流畅。

路由器将导航包装在 Transitions 中:

function searchAction(value) {
    router.setParams('q', value);
}

更新搜索参数是异步的,会更改 URL 并触发数据重新获取,同时 Transition 会跟踪这一切。

数据层将 use() 与缓存的 Promise 结合使用:

function LessonList({ tab, search, completeAction }) {
    const lessons = use(data.getLessons(tab, search));
    return (
        <Design.List>
            {lessons.map(item => (
                <Lesson item={item} completeAction={completeAction} />
            ))}
        </Design.List>
    );
}

当数据加载时,组件会挂起,Suspense 在初始加载时显示后备方案,但在切换标签页和搜索期间,Transitions 会保持旧内容可见。

Design 组件暴露 Action 属性:

<Design.SearchInput value={search} changeAction={searchAction} />

SearchInput 在内部使用 useOptimistic,以便在新的 URL 的 Transition 处于待处理状态时立即更新输入值。TabList 同样乐观地更新选中的标签页。

命名约定(“changeAction”)表示传递的函数将在 Transition 中运行。

状态变更以相同方式工作:

async function completeAction(id) {
    await data.mutateToggle(id);
    router.refresh();
}

这个 completeAction 通过 LessonList 传递给 Design.CompleteButton,该按钮也暴露了一个 action 属性。该按钮在 Action 运行时乐观地更新完成状态。


这是一个简化版的课程应用示例:

export default function Home() {
    const router = useRouter();
    const search = router.search.q || '';
    const tab = router.search.tab || 'all';

    function searchAction(value) {
        router.setParams('q', value);
    }

    function tabAction(value) {
        router.setParams('tab', value);
    }

    async function completeAction(id) {
        await data.mutateToggle(id);
        router.refresh();
    }

    return (
        <>
            <Design.SearchInput value={search} changeAction={searchAction} />
            <Design.TabList activeTab={tab} changeAction={tabAction}>
                <Suspense fallback={<Design.FallbackList />}>
                    <LessonList tab={tab} search={search} completeAction={completeAction} />
                </Suspense>
            </Design.TabList>
        </>
    );
}

协调发生在每个层面:

  • 路由:导航被包装在 Transitions 中。
  • 数据获取:数据层使用 Suspense 和缓存的 Promise。
  • 设计组件:组件暴露“Action”属性以在内部处理乐观更新。

在快速网络上,更新是即时的。在慢速网络上,乐观 UI 和 Transitions 在没有手动逻辑的情况下保持响应性。原语的复杂性由路由器、数据获取设置和设计系统处理。应用代码只需将它们连接起来。


构建自定义异步组件

大多数应用可能会使用已经实现了这些模式的库中的组件。但你也可以自己实现它们来构建自定义异步组件。

这是一个针对 Next.js 的实用示例:一个与 URL 参数同步的可复用选择组件。

这对于过滤器、排序或任何你希望持久化在 URL 中的 UI 状态很有用:

import { useRouter, useSearchParams } from 'next/navigation';

export function RouterSelect({ name, value, options, selectAction }) {
    const [optimisticValue, setOptimisticValue] = useOptimistic(value);
    const [isPending, startTransition] = useTransition();
    const router = useRouter();
    const searchParams = useSearchParams();

    function changeAction(e) {
        const newValue = e.target.value;
        startTransition(async () => {
            setOptimisticValue(newValue);
            await selectAction?.(newValue);

            const params = new URLSearchParams(searchParams);
            params.set(name, newValue);
            router.push(`?${params.toString()}`);
        });
    }

    return (
        <select name={name} value={optimisticValue} onChange={changeAction} style={{ opacity: isPending ? 0.7 : 1 }}>
            {options.map(opt => (
                <option key={opt.value} value={opt.value}>
                    {opt.label}
                </option>
            ))}
        </select>
    );
}

该组件在内部处理协调。父组件可以通过 selectAction 注入副作用:

function Filters() {
    const [progress, setProgress] = useState(0);
    const [optimisticProgress, incrementProgress] = useOptimistic(progress, (prev, increment) => prev + increment);

    return (
        <>
            <LoadingBar progress={optimisticProgress} />
            <RouterSelect
                name='category'
                selected={selectedCategory}
                options={categoryOptions}
                selectAction={items => {
                    incrementProgress(30);
                    setProgress(100);
                }}
            />
        </>
    );
}

在这个例子中,进度条的乐观更新和路由器导航被协调在一起。传递给 selectAction 的任何内容都受益于相同的异步协调。命名约定(“Action”)表示它在 Transition 中运行,并且我们可以在内部调用乐观更新。

这就是 Async React 示例中设计组件使用的模式。SearchInputTabList 和 CompleteButton 都暴露了 Action 属性,在内部处理 Transitions、乐观更新和待处理状态。

使用 ViewTransition(Canary)实现平滑动画

原语解决了更新 何时 发生的问题,而 ViewTransition 则解决了它们 看起来如何 的问题。它包装了浏览器的 View Transition API,并专门在 React Transition(由 useTransitionuseDeferredValue 或 Suspense 触发)内部更新组件时激活。

默认情况下,它在状态之间进行交叉淡入淡出,你也可以使用 CSS 自定义动画。

以下是 Async React 示例如何使用它为课程列表添加动画:

return (
    <ViewTransition key='results' default='none' enter='auto' exit='auto'>
        <Design.List>
            {lessons.map(item => (
                <ViewTransition key={item.id}>
                    <Lesson item={item} completeAction={completeAction} />
                </ViewTransition>
            ))}
        </Design.List>
    </ViewTransition>
);

外层的 ViewTransition 在 Suspense 解析或在状态之间切换时(如显示“无结果”)为整个列表添加动画。每个项目上的内层 ViewTransition 为单个课程添加动画:搜索时,现有项目滑动到新位置,而新项目淡入,移除的项目淡出。

注意:  ViewTransition 目前仅在 React 的 canary 版本中可用。

实际权衡

采用这些模式通常比它们所替代掉的手动逻辑更简单。你并没有增加复杂性;而是将协调工作丢给了 React。话虽如此,以 Transitions、乐观更新和 Suspense 边界的方式思考确实需要思维转变。

何时适用

这些原语在具有丰富交互性的应用中表现出色:仪表板、管理面板和搜索界面。它们消除了整类的 Bug。竞态条件消失了。导航感觉无缝。你可以用更少的样板代码获得“原生应用”的感觉。

不要修复未损坏的东西

如果 useState 和 useEffect 对你来说工作可靠,就没有必要拆除它们。如果你没有在处理竞态条件、突兀的加载状态或输入延迟,你就不需要解决不存在的问题。

迁移路径

你可以选择渐进式的采用。下次构建具有复杂异步状态的功能时,可以尝试用 Transition 代替另一个 isLoading 标识。在即时反馈重要的地方添加乐观 UI。这些工具与现有代码共存,因此你可以逐个功能地采用它们。

结论:向声明式异步的转变

异步 React(Async React)是并发渲染和协调原语的结合,形成了一个用于处理异步工作的完整系统,而这在过去需要手动编排。

随着这些原语在整个生态系统中被采用,这种转变变得切实可行。在 React Conf 2025 上宣布的 Async React 工作组 正在积极致力于在路由器、数据获取库和设计组件中标准化这些模式。

我们已经看到它的实际应用:

  • 路由器(如 Next.js)默认将导航包装在 Transitions 中。
  • 数据库(如 TanStack Query 和 SWR)深度集成了对 Suspense 的支持。
  • 设计系统预计将跟进,暴露 Action 属性以在内部处理待处理状态和乐观更新。

最终,这将异步处理的复杂性从应用代码转移到了框架。你描述 应该发生什么(Action、状态变更、导航),而 React 协调 它如何发生(待处理状态、乐观更新、加载边界)。React 的下一个时代不仅是关于新功能;更是关于让无缝的异步协调成为应用功能的默认方式。

React 已经改变了,你的 Hooks 也应该改变

2025年12月22日 14:16

原文: React has changed, your Hooks should too

翻译: 嘿嘿

来源:前端周刊

React Hooks 已经问世多年,但大多数代码库仍然以同样的方式使用它们:用点 useState,过度使用 useEffect,以及大量不经思考就复制粘贴的模式。我们都经历过。

但 Hooks 从来就不是生命周期方法的简单重写。它们是用于构建更具表现力、更模块化架构的设计系统。

随着并发式 React(React 18/19 时代)的到来,React 处理数据(尤其是异步数据)的方式已经改变。我们现在有了服务器组件、use()、服务器操作、基于框架的数据加载……甚至根据你的设置,在客户端组件中也具备了一些异步能力。

那么,让我们来看看现代 Hook 模式如今是什么样子,React 在引导开发者走向何方,以及生态系统不断陷入的陷阱。

useEffect 陷阱:做得太多、太频繁

useEffect 仍然是最常被滥用的 Hook。它常常成为堆放不应属于那里的逻辑的“垃圾场”,例如数据获取、衍生值,甚至简单的状态转换。这通常就是组件开始感觉“诡异”的时候:它们在不恰当的时间重新运行,或者运行得过于频繁。

useEffect(() => {
  // 每次查询变化时都会重新运行,即使新值实际上相同
  fetchData();
}, [query]);

这种痛苦大部分源于将衍生状态副作用混在一起,而 React 对这两者的处理方式截然不同。

以 React 预期的方式使用副作用

React 在这里的规则出奇地简单:

只在真正有必要时才使用副作用。

其他一切都应该在渲染过程中衍生出来。

const filteredData = useMemo(() => {
  return data.filter(item => item.includes(query));
}, [data, query]);

当你确实需要一个副作用时,React 的 useEffectEvent 会是你的好帮手。它让你能在副作用内部访问最新的 props/状态,而不必扰乱你的依赖数组。

const handleSave = useEffectEvent(async () => {
  await saveToServer(formData);
});

在使用 useEffect 之前,先问问自己:

  • 这是由外部因素(网络、DOM、订阅)驱动的吗?
  • 还是我可以在渲染过程中计算这个?

如果是后者,像 useMemouseCallback 或框架提供的基础构建块这样的工具,会让你的组件健壮得多。

🙋🏻‍♂️ 小贴士

不要把 useEffectEvent 当作一种用来逃避编写依赖数组(dependency arrays)的‘作弊码’。它是专门针对 Effect 内部的操作逻辑进行优化的。”

自定义 Hooks:不仅仅是复用,更是真正的封装

自定义 Hooks 不仅仅是为了减少重复代码。它们关乎将领域逻辑从组件中抽离出来,让你的 UI 专注于……嗯,UI。

例如,与其用这样的设置代码来污染组件:

useEffect(() => {
  const listener = () => setWidth(window.innerWidth);
  window.addEventListener('resize', listener);
  return () => window.removeEventListener('resize', listener);
}, []);

不如将其移入一个 Hook:

function useWindowWidth() {
  const [width, setWidth] = useState(
    typeof window !== 'undefined' ? window.innerWidth : 0
  );

  useEffect(() => {
    const listener = () => setWidth(window.innerWidth);
    window.addEventListener('resize', listener);
    // 注意:原文为 'change',但通常 resize 事件应配对 'resize',这里保持原文但应该是笔误
    return () => window.removeEventListener('change', listener);
  }, []);

  return width;
}

这样就干净多了。也更容易测试。你的组件不再泄露实现细节。

SSR 小提示

总是从确定的回退值开始,以避免水合不匹配报错。

基于订阅的状态与 useSyncExternalStore

React 18 引入了 useSyncExternalStore,它悄无声息地解决了一大类与订阅、撕裂效应和高频更新相关的 Bug。

如果你曾经与 matchMedia、滚动位置或跨渲染行为不一致的第三方存储库斗争过,这就是 React 希望你使用的 API。

它适用于:

  • 浏览器 API(matchMedia、页面可见性、滚动位置)
  • 外部存储(Redux、Zustand、自定义订阅系统)
  • 任何对性能敏感或事件驱动的事物
function useMediaQuery(query) {
  return useSyncExternalStore(
    (callback) => {
      const mql = window.matchMedia(query);
      mql.addEventListener('change', callback);
      return () => mql.removeEventListener('change', callback);
    },
    () => window.matchMedia(query).matches,
    () => false // SSR 回退值
  );
}

使用过渡和延迟值实现更流畅的 UI

如果你的应用在用户输入或筛选时感觉卡顿,React 的并发工具可以提供帮助。这些并非魔法,但它们能帮助 React 将紧急更新置于高开销更新之前。

const [searchTerm, setSearchTerm] = useState('');
const deferredSearchTerm = useDeferredValue(searchTerm);

const filtered = useMemo(() => {
  return data.filter(item => item.includes(deferredSearchTerm));
}, [data, deferredSearchTerm]);

输入保持响应,而繁重的筛选工作被延后处理。

快速心智模型:

  • startTransition(() => setState()) → 延迟状态更新
  • useDeferredValue(value) → 延迟衍生值

需要时可以一起使用,但不要过度使用。它们不适用于琐碎的计算。

可测试和可调试的 Hooks

现代 React DevTools 让检查自定义 Hooks 变得极其简单。如果你能良好地组织你的 Hooks,大部分逻辑无需渲染实际组件就能测试。

  • 将领域逻辑与 UI 分离
  • 尽可能直接测试 Hooks
  • 为了清晰,将提供者逻辑提取到其自身的 Hook 中
function useAuthProvider() {
  const [user, setUser] = useState(null);
  const login = async (credentials) => { /* ... */ };
  const logout = () => { /* ... */ };
  return { user, login, logout };
}

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const value = useAuthProvider();
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth() {
  return useContext(AuthContext);
}

下次调试时,你会感谢自己这么做。

超越 Hooks:迈向数据优先的 React 应用

React 正朝着数据优先的渲染流程转变,特别是现在服务器组件和基于操作的模式正在成熟。它并非追求像 Solid.js 那样的细粒度响应式,但 React 正大力投入异步数据和服务器驱动的 UI。

值得了解的 API:

  • use() 用于在渲染期间处理异步资源(主要用于服务器组件;通过服务器操作在客户端组件中支持有限)
  • useEffectEvent 用于稳定的副作用回调
  • useActionState 用于类似工作流的异步状态
  • 框架级别的缓存和数据原语
  • 更好的并发渲染工具和 DevTools

方向很明确:React 希望我们减少对“瑞士军刀”式 useEffect 的依赖,更多地依赖简洁、由渲染驱动的数据流。

围绕衍生状态和服务器/客户端边界来设计你的 Hooks,能让你的应用天然地面向未来。

Hooks 即架构,而非语法

Hooks 不仅仅是比类组件更友好的 API,它们是一种架构模式。

  • 将衍生状态放在渲染过程中
  • 只将副作用用于真正的副作用
  • 通过小而专注的 Hooks 组合逻辑
  • 让并发工具平滑处理异步流程
  • 同时考虑客户端服务器边界

React 在进化,我们的 Hooks 也应随之进化。

如果你仍然在用 2020 年的方式写 Hooks,那也没关系。我们大多数人都是如此。但 React 18+ 给了我们一个强大得多的工具箱,熟悉这些模式会很快带来回报。

TypeScript 严格性是非单调的:strict-null-checks 和 no-implicit-any 的相互影响

2025年12月22日 14:15

原文: TypeScript strictness is non-monotonic: strict-null-checks and no-implicit-any interact

翻译: 嘿嘿

来源:前端周刊

TypeScript 编译器选项 strictNullChecksnoImplicitAny 以一种奇怪的方式相互作用:仅启用 strictNullChecks 会导致类型错误,而在同时启用 noImplicitAny 后这些错误却消失了。这意味着更严格的设置反而导致更少的错误!

这虽然是一个影响不大的奇闻异事,但我在实际工作中确实遇到了它,当时我正在将一些模块更新为更严格的设置。

背景

TypeScript 是驯服 JavaScript 代码库的强大工具,但要获得最大的保障,需要在“严格”模式下使用它。

在现有的 JavaScript 代码库中采用 TypeScript 可以逐步完成:逐个打开每个严格的子设置,并逐一处理出现的错误。这种渐进式方法使得采用变得可行:不要在一次大爆炸中修复整个世界,而是进行多次较小的更改,直到最终世界被修复。

在工作中,我们最近一直在以这种方式逐步提高代码的严格性,然后我遇到了这种相互作用。

示例

下面这段代码中,array 的类型是什么?

const array = [];
array.push(123);

作为一个独立的代码片段,它看起来奇怪且毫无意义(“为什么不直接用 const array = [123];?”),但它是真实代码的最小化版本。

const featureFlags = [];

if (enableRocket()) {
  featureFlags.push("rocket");
}
if (enableParachute()) {
  featureFlags.push("parachute");
}

prepareForLandSpeedRecord(featureFlags);

这里没有显式的类型注解,所以 TypeScript 需要推断它。这种推断有点巧妙,因为它需要“时间旅行”(指需要运行后续语句后回头去修改推断的类型,类似正则回溯):const array = [] 这个声明并没有说明数组中可能包含什么,这个信息只来自代码后面出现的 push

考虑到所有这些,推断出的确切类型依赖于两个 TypeScript 语言选项也就不足为奇了:

strictNullChecks noImplicitAny 推断类型
最不严格 any[]
number[]
never[]
最严格 number[]

选项说明

这里影响推断类型的两个选项是:

  • strictNullChecks:正确强制处理可选/可为空的值。例如,启用后,一个可为空的字符串变量(类型为 string | null)不能直接用在期望普通 string 值的地方。
  • noImplicitAny:避免在一些模棱两可的情况下推断出“全能”的 any 类型。

最好同时启用它们:strictNullChecks 解决了“十亿美元的错误”,而 noImplicitAny 减少了感染代码库的容易出错的 any 的数量。

问题所在

我们上表中第三种配置,即启用 strictNullChecks 但禁用 noImplicitAny 时,推断出 array: never[]。因此,代码片段无效并被报错(在线示例):

array.push(123);
//         ^^^ 错误:类型“123”的参数不能赋给类型“never”的参数。

没有任何东西(既不是字面量 123,也不是任何其他 number,也不是任何其他东西)是 never 的“子类型”,所以,是的,这段代码无效是合理的。

奇怪之处

“启用一些更严格的要求,然后得到一个错误”并不令人惊讶,也不值得注意……但让我们再仔细看看表格:

strictNullChecks noImplicitAny 推断类型
最不严格 any[]
number[]
报错! never[]
最严格 number[]

所以,如果我们从一个宽松的代码库开始,并希望使其变得严格,我们可能会:

  1. 启用 strictNullChecks,然后遇到一个新错误(不奇怪),然后
  2. 解决这个错误,无需更改代码,只需启用 noImplicitAny(奇怪!)。

当我们朝着完全严格的方向前进时,逐个启用严格选项可能会导致一些“虚假的”错误短暂出现,仅仅出现在中间的半严格状态。随着我们打开设置,错误数量会先上升后下降!

我个人期望启用严格选项是单调的:启用的选项越多 = 报错越多。但这一对选项违反了这种期望。

解决方案

在尝试使 TypeScript 代码库变得严格时,有几种方法可以“解决”这种奇怪现象:

  1. 直接用显式注解修复错误,例如 const array: number[] = []
  2. 使用不同的逐个启用顺序:先启用 noImplicitAny,然后再启用 strictNullChecks。如上表所示,按照这个顺序,两个步骤的推断结果都是 array: number[],因此没有错误。
  3. 同时启用它们:不要试图完全渐进,而是将这两个选项作为一步启用。

解释

为什么启用 strictNullChecks 并禁用 noImplicitAny 会导致一个在其他地方不出现的错误?jcalz 在 StackOverflow 上解释得很好,其核心是:

  • 这种有问题的组合是一个为了向后兼容而留下的边缘情况,其中 array 的类型在其声明处被推断为 never[],并在后续代码中被锁定。
  • 启用 noImplicitAny 会使编译器在模棱两可的位置(在没有 noImplicitAny 时会推断为 any 的地方)使用“演化”类型(evolving types,可理解为先推断为 any/never 然后后续追加推断的类型):因此,array 的类型不会在其声明行被确定,并且可以结合来自 push 的信息进行推断。

评论

这感觉像是一个有趣的脑筋急转弯,而不是一个重大问题:

  • 修复这些虚假错误并不是一个重大的负担或显著的浪费时间,而且可以说,添加注解可能使这类代码更清晰。
  • 半严格状态可能有奇怪的行为是可以理解的:我想 TypeScript 开发者更关心完全严格模式下的良好体验,希望中间状态只是垫脚石,而不是长期状态。

总结

TypeScript 选项 strictNullChecksnoImplicitAny 以一种奇怪的方式相互作用:以“错误”的顺序逐个启用它们会导致错误出现然后又消失,违反了单调性的期望(启用的严格选项越多 = 错误越多)。这可能发生在真实代码中,但影响极小,因为很容易解决和/或规避。

现代 JavaScript 特性:TypeScript 深度解析与实践

作者 Fronty
2025年12月22日 14:05

引言

TypeScript作为JavaScript的超集,为现代前端开发带来了强大的类型系统和面向对象编程能力。它不仅在编译时提供类型检查,还支持ECMAScript的最新特性,极大地提高了代码的可维护性和开发体验。本文将深入探讨TypeScript的核心特性,包括类型检查、接口泛型、装饰器等底层实现原理,并补充类型推断、高级类型、工程化实践等关键内容,通过完整代码示例展示TypeScript在真实项目中的应用价值。

一、类型检查的简单实现

运行时类型检查的基础

TypeScript在编译时进行类型检查,但理解其背后的原理有助于我们实现运行时类型检查。

// 1.1 基础类型检查器
class TypeChecker {
  static isString(value) {
    return typeof value === "string";
  }

  static isNumber(value) {
    return typeof value === "number" && !isNaN(value);
  }

  static isBoolean(value) {
    return typeof value === "boolean";
  }

  static isArray(value) {
    return Array.isArray(value);
  }

  static isObject(value) {
    return value !== null && typeof value === "object" && !Array.isArray(value);
  }

  static isFunction(value) {
    return typeof value === "function";
  }

  static isNull(value) {
    return value === null;
  }

  static isUndefined(value) {
    return typeof value === "undefined";
  }

  // 复合类型检查
  static isType(value, expectedType) {
    switch (expectedType) {
      case "string":
        return this.isString(value);
      case "number":
        return this.isNumber(value);
      case "boolean":
        return this.isBoolean(value);
      case "array":
        return this.isArray(value);
      case "object":
        return this.isObject(value);
      case "function":
        return this.isFunction(value);
      case "null":
        return this.isNull(value);
      case "undefined":
        return this.isUndefined(value);
      default:
        throw new Error(`Unsupported type: ${expectedType}`);
    }
  }
}

console.log(TypeChecker.isString("hello")); // true
console.log(TypeChecker.isNumber(42)); // true
console.log(TypeChecker.isType([1, 2, 3], "array")); // true

// 1.2 增强的类型检查器
class EnhancedTypeChecked {
  static types = {
    string: (value) => typeof value === "string",
    number: (value) => typeof value === "number" && !isNaN(value),
    boolean: (value) => typeof value === "boolean",
    array: (value) => Array.isArray(value),
    object: (value) =>
      value !== null && typeof value === "object" && !Array.isArray(value),
    function: (value) => typeof value === "function",
    null: (value) => value === null,
    undefined: (value) => typeof value === "undefined",
    // 自定义类型
    email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
    uuid: (value) =>
      /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
        value
      ),
    date: (value) => value instanceof Date,
    // 联合类型
    union: (value, types) => types.some((type) => this.check(value, type)),
    // 字面量类型
    literal: (value, expected) => vaue === expected,
  };

  static check(value, type) {
    if (typeof type === "string") {
      const checker = this.types[type];
      return checker ? checker(value) : false;
    }
    if (typeof type === "object" && type.type) {
      if (type.type === "union") {
        return this.types.union(value, type.types);
      }
      if (type.type === "literal") {
        return this.types.literal(value, type.value);
      }
    }

    return false;
  }

  static validate(value, schema) {
    if (typeof schema === "string") {
      return this.check(value, schema);
    }

    if (Array.isArray(schema)) {
      // 数组类型
      if (!this.check(value, "array")) {
        return false;
      }
      const [itemType] = schema;
      return value.every((item) => this.validate(item, itemType));
    }

    if (typeof schema === "object" && !Array.isArray(schema)) {
      // 对象类型
      if (!this.check(value, "object")) return false;

      for (const [key, propSchema] of Object.entries(schema)) {
        if (!this.validate(value[key], propSchema)) {
          return false;
        }
      }

      return true;
    }
    return false;
  }
}

// 使用示例
const schema = {
  name: "string",
  age: "number",
  email: "email",
  tags: ["string"],
  address: {
    street: "string",
    city: "string",
  },
};

const data = {
  name: "John",
  age: 30,
  email: "john@example.com",
  tags: ["developer", "typescript"],
  address: {
    street: "123 Main St",
    city: "New York",
  },
};

console.log(EnhancedTypeChecked.validate(data, schema)); // true

// 1.3 编译时类型检查模拟
function createTypeSafeProxy(target, schema) {
  return new Proxy(target, {
    set(obj, prop, value) {
      if (schema[prop]) {
        const isValid = EnhancedTypeChecked.validate(value, schema[prop]);
        if (!isValid) {
          throw new TypeError(
            `Invalid type for property ${prop}. Expected ${JSON.stringify(
              schema[prop]
            )}, got ${typeof value}`
          );
        }
      }
      obj[prop] = value;
      return true;
    },
    get(obj, prop) {
      return obj[prop];
    },
  });
}

const userSchema = {
  name: "string",
  age: "number",
  active: "boolean",
};

const user = createTypeSafeProxy({}, userSchema);

try {
  user.name = "Alice"; // 成功
  user.age = "25"; // Invalid type for property age. Expected "number", got string
} catch (error) {
  console.error(error.message);
}
TypeScript编译器模拟
class SimpleTypeScriptCompiler {
  constructor() {
    this.typeRegistry = new Map();
    this.errors = [];
  }

  // 解析类型注释
  parseTypeAnnotation(code) {
    const typeRegex = /:\s*([^{}\[\]]+|\{[^}]+\}|\[[^\]]+\])/g;
    const matches = [];
    let match;

    while ((match = typeRegex.exec(code)) !== null) {
      matches.push(match[1].trim());
    }

    return matches;
  }

  // 提取接口定义
  extractInterface(code) {
    const interfaceRegex = /interface\s+(\w+)\s*\{([^}]+)\}/g;
    const interfaces = new Map();
    let match;

    while ((match = interfaceRegex.exec(code)) !== null) {
      const [, name, body] = match;
      const properties = {};

      body.split(";").forEach((line) => {
        const propMatch = line.trim().match(/(\w+)\s*:\s*([^;]+)/);
        if (propMatch) {
          const [, propName, propType] = propMatch;
          properties[propName] = propType.trim();
        }
      });

      interfaces.set(name, properties);
    }

    return interfaces;
  }

  // 检查函数类型
  checkFunctionTypes(code, interfaces) {
    const functionRegex =
      /function\s+(\w+)\(([^)]*)\)\s*(?::\s*([^{]+))?\s*\{/g;
    const errors = [];
    let match;

    while ((match = functionRegex.exec(code)) !== null) {
      const [, functionName, params, returnType] = match;

      // 检查参数类型
      if (params) {
        params.split(",").forEach((param) => {
          const paramMatch = param.match(/(\w+)\s*:\s*(\w+)/);
          if (paramMatch) {
            const [, paramName, paramType] = paramMatch;
            // 这里可以添加实际类型检查逻辑
          }
        });
      }

      // 这里可以添加返回值类型检查逻辑
    }

    return errors;
  }

  // 编译TypeScript代码
  compile(code) {
    this.errors = [];

    // 提取接口
    const interfaces = this.extractInterface(code);

    // 检查类型
    const typeErrors = this.checkFunctionTypes(code, interfaces);
    this.errors.push(...typeErrors);

    // 移除类型注释, 生成JavaScript代码
    let jsCode = code
      .replace(/:\s*([^{}\[\]]+|\{[^}]+\}|\[[^\]]+\])/g, "") // 移除参数和变量类型
      .replace(/interface\s+\w+\s*\{[^}]+\}/g, "") // 移除接口定义
      .replace(/\s*\/\/.*$/gm, "") // 移除注释
      .trim();

    return {
      jsCode,
      errors: this.errors,
      interfaces: Array.from(interfaces.entries()),
    };
  }
}

// 使用示例
const tsCode = `
interface User {
  name: string;
  age: number;
}

function greet(user: User): string {
  return "Hello, " + user.name;
}

const john: User = { name: "John", age: 30 };
console.log(greet(john));
`;

const compiler = new SimpleTypeScriptCompiler();
const result = compiler.compile(tsCode);

console.log("生成的JavaScript代码:");
console.log(result.jsCode);
console.log("提取的接口:", result.interfaces);
console.log("类型错误:", result.errors);

二、接口和泛型的模拟

接口的实现与模拟
// 2.1 接口检查器
class InterfaceChecker {
  static interfaces = new Map();
  
  // 定义接口
  static defineInterface(name, structure) {
    this.interfaces.set(name, structure);
  }
  
  // 检查对象是否实现接口
  static implements(obj, interfaceName) {
    const interfaceDef = this.interfaces.get(interfaceName);
    if (!interfaceDef) {
      throw new Error(`接口 ${interfaceName} 未定义`);
    }
    
    // 检查所有必需属性
    for (const [prop, type] of Object.entries(interfaceDef.required || {})) {
      if (!(prop in obj)) {
        return false;
      }
      if (type && !this.checkType(obj[prop], type)) {
        return false;
      }
    }
    
    // 检查可选属性类型(如果存在)
    for (const [prop, type] of Object.entries(interfaceDef.optional || {})) {
      if (prop in obj && type && !this.checkType(obj[prop], type)) {
        return false;
      }
    }
    
    // 检查方法
    for (const method of interfaceDef.methods || []) {
      if (!(method in obj) || typeof obj[method] !== 'function') {
        return false;
      }
    }
    
    return true;
  }
  
  // 类型检查
  static checkType(value, type) {
    if (typeof type === 'string') {
      switch (type) {
        case 'string': return typeof value === 'string';
        case 'number': return typeof value === 'number' && !isNaN(value);
        case 'boolean': return typeof value === 'boolean';
        case 'object': return value !== null && typeof value === 'object';
        default: return true;
      }
    }
    
    if (Array.isArray(type)) {
      return Array.isArray(value) && value.every(item => this.checkType(item, type[0]));
    }
    
    return true;
  }
  
  // 创建实现接口的类
  static createClass(interfaceName, implementation) {
    const interfaceDef = this.interfaces.get(interfaceName);
    if (!interfaceDef) {
      throw new Error(`接口 ${interfaceName} 未定义`);
    }
    
    // 创建类
    const DynamicClass = class {
      constructor(...args) {
        if (implementation.constructor) {
          implementation.constructor.apply(this, args);
        }
        
        // 验证实例是否符合接口
        if (!this.implements(interfaceName)) {
          throw new Error(`类未正确实现接口 ${interfaceName}`);
        }
      }
      
      implements(interfaceName) {
        return InterfaceChecker.implements(this, interfaceName);
      }
    };
    
    // 添加接口要求的方法和属性
    for (const [prop, descriptor] of Object.entries(implementation)) {
      if (prop !== 'constructor') {
        Object.defineProperty(DynamicClass.prototype, prop, descriptor);
      }
    }
    
    return DynamicClass;
  }
}

// 定义接口
InterfaceChecker.defineInterface('Serializable', {
  required: {
    toJSON: 'function',
    fromJSON: 'function'
  }
});

InterfaceChecker.defineInterface('User', {
  required: {
    id: 'number',
    name: 'string'
  },
  optional: {
    email: 'string',
    age: 'number'
  },
  methods: ['save', 'delete']
});

// 2.2 使用接口
const UserClass = InterfaceChecker.createClass('User', {
  constructor: function(id, name) {
    this.id = id;
    this.name = name;
  },
  
  save: {
    value: function() {
      console.log(`保存用户 ${this.name}`);
      return true;
    }
  },
  
  delete: {
    value: function() {
      console.log(`删除用户 ${this.name}`);
      return true;
    }
  },
  
  toJSON: {
    value: function() {
      return {
        id: this.id,
        name: this.name
      };
    }
  }
});

const user = new UserClass(1, 'Alice');
console.log(user.implements('User')); // true
console.log(user.implements('Serializable')); // false

// 2.3 运行时接口验证装饰器
function implementsInterface(interfaceName) {
  return function(target) {
    const originalConstructor = target;
    
    // 返回新的构造函数
    const newConstructor = function(...args) {
      const instance = new originalConstructor(...args);
      
      if (!InterfaceChecker.implements(instance, interfaceName)) {
        throw new Error(`类 ${target.name} 未实现接口 ${interfaceName}`);
      }
      
      return instance;
    };
    
    // 复制原型链
    newConstructor.prototype = originalConstructor.prototype;
    
    return newConstructor;
  };
}

// 使用装饰器
@interface('User')
class AdminUser {
  constructor(id, name) {
    this.id = id;
    this.name = name;
  }
  
  save() {
    console.log('保存管理员');
  }
  
  delete() {
    console.log('删除管理员');
  }
}

try {
  const admin = new AdminUser(1, 'Admin');
  console.log('AdminUser创建成功');
} catch (error) {
  console.error(error.message);
}
泛型的实现与模拟
// 2.4 泛型容器类
class GenericContainer {
  constructor(value) {
    this._value = value;
  }
  
  get value() {
    return this._value;
  }
  
  set value(newValue) {
    this._value = newValue;
  }
  
  // 泛型方法:转换值类型
  map(transformer) {
    return new GenericContainer(transformer(this._value));
  }
  
  // 检查类型(运行时)
  checkType(expectedType) {
    return typeof this._value === expectedType;
  }
}

// 使用示例
const numberContainer = new GenericContainer(42);
console.log(numberContainer.value); // 42
console.log(numberContainer.checkType('number')); // true

const stringContainer = numberContainer.map(n => `数字: ${n}`);
console.log(stringContainer.value); // "数字: 42"
console.log(stringContainer.checkType('string')); // true

// 2.5 泛型函数工厂
function createGenericFunction(typeCheck) {
  return {
    // 泛型身份函数
    identity: function(value) {
      if (typeCheck && !typeCheck(value)) {
        throw new TypeError('类型不匹配');
      }
      return value;
    },
    
    // 泛型数组操作
    filterArray: function(arr, predicate) {
      if (!Array.isArray(arr)) {
        throw new TypeError('第一个参数必须是数组');
      }
      return arr.filter(predicate);
    },
    
    // 泛型映射
    mapArray: function(arr, mapper) {
      if (!Array.isArray(arr)) {
        throw new TypeError('第一个参数必须是数组');
      }
      return arr.map(mapper);
    }
  };
}

// 创建特定类型的泛型函数
const numberFunctions = createGenericFunction(val => typeof val === 'number');
const stringFunctions = createGenericFunction(val => typeof val === 'string');

console.log(numberFunctions.identity(123)); // 123
console.log(stringFunctions.identity('hello')); // "hello"

// 2.6 泛型约束模拟
class GenericValidator {
  static validate(value, constraints) {
    // 类型约束
    if (constraints.type && typeof value !== constraints.type) {
      return false;
    }
    
    // 最小/最大值约束
    if (constraints.min !== undefined && value < constraints.min) {
      return false;
    }
    
    if (constraints.max !== undefined && value > constraints.max) {
      return false;
    }
    
    // 长度约束
    if (constraints.minLength !== undefined && value.length < constraints.minLength) {
      return false;
    }
    
    if (constraints.maxLength !== undefined && value.length > constraints.maxLength) {
      return false;
    }
    
    // 自定义验证函数
    if (constraints.validator && !constraints.validator(value)) {
      return false;
    }
    
    return true;
  }
  
  // 创建受约束的泛型类
  static createConstrainedClass(constraints) {
    return class ConstrainedContainer {
      constructor(value) {
        if (!GenericValidator.validate(value, constraints)) {
          throw new Error('值不符合约束条件');
        }
        this._value = value;
      }
      
      get value() {
        return this._value;
      }
      
      set value(newValue) {
        if (!GenericValidator.validate(newValue, constraints)) {
          throw new Error('新值不符合约束条件');
        }
        this._value = newValue;
      }
    };
  }
}

// 使用泛型约束
const NumberContainer = GenericValidator.createConstrainedClass({
  type: 'number',
  min: 0,
  max: 100
});

const StringContainer = GenericValidator.createConstrainedClass({
  type: 'string',
  minLength: 2,
  maxLength: 50
});

try {
  const numContainer = new NumberContainer(50); // 成功
  console.log(numContainer.value); // 50
  
  const strContainer = new StringContainer('Hello'); // 成功
  console.log(strContainer.value); // "Hello"
  
  // numContainer.value = 150; // 抛出错误
} catch (error) {
  console.error(error.message);
}

// 2.7 泛型工具类型模拟
class GenericUtils {
  // 模拟 Partial<T>
  static partial(obj) {
    const result = {};
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        result[key] = obj[key];
      }
    }
    return result;
  }
  
  // 模拟 Readonly<T>
  static readonly(obj) {
    const result = {};
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        Object.defineProperty(result, key, {
          value: obj[key],
          writable: false,
          enumerable: true,
          configurable: false
        });
      }
    }
    return Object.freeze(result);
  }
  
  // 模拟 Pick<T, K>
  static pick(obj, keys) {
    const result = {};
    keys.forEach(key => {
      if (key in obj) {
        result[key] = obj[key];
      }
    });
    return result;
  }
  
  // 模拟 Omit<T, K>
  static omit(obj, keys) {
    const result = {};
    for (const key in obj) {
      if (obj.hasOwnProperty(key) && !keys.includes(key)) {
        result[key] = obj[key];
      }
    }
    return result;
  }
  
  // 模拟 Record<K, T>
  static record(keys, valueCreator) {
    const result = {};
    keys.forEach(key => {
      result[key] = valueCreator(key);
    });
    return result;
  }
}

// 使用泛型工具
const user = {
  id: 1,
  name: 'John',
  email: 'john@example.com',
  age: 30
};

console.log(GenericUtils.pick(user, ['id', 'name'])); // { id: 1, name: 'John' }
console.log(GenericUtils.omit(user, ['email', 'age'])); // { id: 1, name: 'John' }

const readonlyUser = GenericUtils.readonly(user);
// readonlyUser.name = 'Bob'; // 在严格模式下会抛出错误

三、装饰器的实现原理

装饰器基础实现
// 3.1 基础装饰器工厂
function createDecorator(decoratorFunc) {
  return function(...args) {
    // 类装饰器
    if (args.length === 1 && typeof args[0] === 'function') {
      const targetClass = args[0];
      return decoratorFunc(targetClass) || targetClass;
    }
    
    // 方法装饰器
    if (args.length === 3 && typeof args[2] === 'object') {
      const [target, property, descriptor] = args;
      return decoratorFunc(target, property, descriptor) || descriptor;
    }
    
    // 属性装饰器
    if (args.length === 2 && typeof args[1] === 'string') {
      const [target, property] = args;
      decoratorFunc(target, property);
      return;
    }
    
    // 参数装饰器
    if (args.length === 3 && typeof args[2] === 'number') {
      const [target, property, parameterIndex] = args;
      decoratorFunc(target, property, parameterIndex);
      return;
    }
    
    return decoratorFunc(...args);
  };
}

// 3.2 类装饰器
function classDecorator(logMessage) {
  return createDecorator((targetClass) => {
    // 保存原始构造函数
    const originalConstructor = targetClass;
    
    // 创建新的构造函数
    const newConstructor = function(...args) {
      console.log(`${logMessage}: 创建 ${originalConstructor.name} 实例`);
      const instance = new originalConstructor(...args);
      
      // 可以在这里修改实例
      if (logMessage.includes('sealed')) {
        Object.seal(instance);
      }
      
      return instance;
    };
    
    // 复制原型链
    newConstructor.prototype = originalConstructor.prototype;
    
    // 复制静态属性
    Object.setPrototypeOf(newConstructor, originalConstructor);
    
    return newConstructor;
  });
}

// 3.3 方法装饰器
function methodDecorator(options = {}) {
  return createDecorator((target, property, descriptor) => {
    const originalMethod = descriptor.value;
    
    descriptor.value = function(...args) {
      // 前置处理
      if (options.log) {
        console.log(`调用方法: ${property}, 参数:`, args);
      }
      
      if (options.measureTime) {
        console.time(`方法 ${property} 执行时间`);
      }
      
      // 执行原始方法
      const result = originalMethod.apply(this, args);
      
      // 后置处理
      if (options.measureTime) {
        console.timeEnd(`方法 ${property} 执行时间`);
      }
      
      if (options.log) {
        console.log(`方法 ${property} 返回:`, result);
      }
      
      return result;
    };
    
    return descriptor;
  });
}

// 3.4 属性装饰器
function propertyDecorator(defaultValue) {
  return createDecorator((target, property) => {
    // 创建私有属性名称
    const privateProp = `_${property}`;
    
    // 定义getter和setter
    Object.defineProperty(target, property, {
      get() {
        return this[privateProp] !== undefined ? this[privateProp] : defaultValue;
      },
      set(value) {
        this[privateProp] = value;
      },
      enumerable: true,
      configurable: true
    });
  });
}

// 3.5 参数装饰器
function parameterDecorator(validator) {
  return createDecorator((target, property, parameterIndex) => {
    // 获取方法的参数验证元数据
    if (!target.__parameterValidations) {
      target.__parameterValidations = new Map();
    }
    
    const key = `${property}_${parameterIndex}`;
    target.__parameterValidations.set(key, validator);
    
    // 重写方法以添加参数验证
    const originalMethod = target[property];
    
    target[property] = function(...args) {
      // 验证参数
      const validator = target.__parameterValidations.get(`${property}_${parameterIndex}`);
      if (validator && !validator(args[parameterIndex])) {
        throw new Error(`参数 ${parameterIndex} 验证失败`);
      }
      
      return originalMethod.apply(this, args);
    };
  });
}

// 使用示例
@classDecorator('自动日志记录 - sealed')
class UserService {
  @propertyDecorator('guest')
  role;
  
  constructor(name) {
    this.name = name;
  }
  
  @methodDecorator({ log: true, measureTime: true })
  greet(@parameterDecorator(val => typeof val === 'string') message) {
    return `${this.name} 说: ${message}`;
  }
  
  @methodDecorator({ log: true })
  setRole(@parameterDecorator(role => ['admin', 'user', 'guest'].includes(role)) newRole) {
    this.role = newRole;
  }
}

const service = new UserService('Alice');
console.log(service.role); // "guest" (默认值)
service.setRole('admin');
console.log(service.greet('你好!')); // 输出日志和结果
高级装饰器模式
// 3.6 依赖注入装饰器
class DependencyContainer {
  static dependencies = new Map();
  
  static register(token, implementation) {
    this.dependencies.set(token, implementation);
  }
  
  static resolve(token) {
    const dependency = this.dependencies.get(token);
    if (!dependency) {
      throw new Error(`未找到依赖: ${token}`);
    }
    
    if (typeof dependency === 'function') {
      return new dependency();
    }
    
    return dependency;
  }
}

function inject(token) {
  return createDecorator((target, property, descriptor) => {
    if (descriptor) {
      // 方法参数注入
      const originalMethod = descriptor.value;
      
      descriptor.value = function(...args) {
        // 解析依赖
        const dependency = DependencyContainer.resolve(token);
        return originalMethod.call(this, dependency, ...args);
      };
      
      return descriptor;
    } else {
      // 属性注入
      return {
        get() {
          return DependencyContainer.resolve(token);
        },
        enumerable: true,
        configurable: true
      };
    }
  });
}

// 3.7 路由装饰器(类似Angular/NestJS)
class Router {
  static routes = new Map();
  
  static get(path) {
    return createDecorator((target, property, descriptor) => {
      const originalMethod = descriptor.value;
      const routeKey = `GET ${path}`;
      
      this.routes.set(routeKey, {
        controller: target.constructor.name,
        method: property,
        handler: originalMethod
      });
      
      descriptor.value = function(...args) {
        console.log(`路由: ${routeKey}`);
        return originalMethod.apply(this, args);
      };
      
      return descriptor;
    });
  }
  
  static post(path) {
    return createDecorator((target, property, descriptor) => {
      const originalMethod = descriptor.value;
      const routeKey = `POST ${path}`;
      
      this.routes.set(routeKey, {
        controller: target.constructor.name,
        method: property,
        handler: originalMethod
      });
      
      return descriptor;
    });
  }
  
  static handleRequest(method, path) {
    const routeKey = `${method} ${path}`;
    const route = this.routes.get(routeKey);
    
    if (route) {
      console.log(`处理请求: ${routeKey}`);
      // 实际中会创建控制器实例并调用方法
      return route.handler;
    }
    
    return null;
  }
}

// 3.8 验证装饰器
class Validator {
  static validate(target, property, value) {
    const validations = target.constructor.__validations;
    if (validations && validations[property]) {
      for (const validation of validations[property]) {
        if (!validation.validator(value)) {
          throw new Error(validation.message || `验证失败: ${property}`);
        }
      }
    }
    return true;
  }
  
  static required(message = '该字段是必填的') {
    return createDecorator((target, property, descriptor) => {
      if (descriptor) {
        // 方法参数验证
        const originalMethod = descriptor.value;
        
        descriptor.value = function(...args) {
          const value = args[0];
          if (!value && value !== 0 && value !== false) {
            throw new Error(message);
          }
          return originalMethod.apply(this, args);
        };
        
        return descriptor;
      } else {
        // 属性验证
        if (!target.constructor.__validations) {
          target.constructor.__validations = {};
        }
        
        if (!target.constructor.__validations[property]) {
          target.constructor.__validations[property] = [];
        }
        
        target.constructor.__validations[property].push({
          validator: value => value != null && value !== '',
          message
        });
        
        // 创建getter/setter
        const privateProp = `_${property}`;
        
        return {
          get() {
            return this[privateProp];
          },
          set(value) {
            this[privateProp] = value;
          },
          enumerable: true,
          configurable: true
        };
      }
    });
  }
  
  static minLength(length, message) {
    return createDecorator((target, property) => {
      if (!target.constructor.__validations) {
        target.constructor.__validations = {};
      }
      
      if (!target.constructor.__validations[property]) {
        target.constructor.__validations[property] = [];
      }
      
      target.constructor.__validations[property].push({
        validator: value => !value || value.length >= length,
        message: message || `长度不能少于 ${length} 个字符`
      });
    });
  }
  
  static maxLength(length, message) {
    return createDecorator((target, property) => {
      if (!target.constructor.__validations) {
        target.constructor.__validations = {};
      }
      
      if (!target.constructor.__validations[property]) {
        target.constructor.__validations[property] = [];
      }
      
      target.constructor.__validations[property].push({
        validator: value => !value || value.length <= length,
        message: message || `长度不能超过 ${length} 个字符`
      });
    });
  }
}

// 使用高级装饰器
@classDecorator('用户控制器')
class UserController {
  @Router.get('/users')
  getAllUsers() {
    return ['Alice', 'Bob', 'Charlie'];
  }
  
  @Router.post('/users')
  createUser(@Validator.required() name) {
    return { id: 1, name };
  }
}

// 用户模型
class User {
  @Validator.required('用户名是必填的')
  @Validator.minLength(3, '用户名至少3个字符')
  @Validator.maxLength(20, '用户名最多20个字符')
  username;
  
  @Validator.required('密码是必填的')
  @Validator.minLength(6, '密码至少6个字符')
  password;
  
  constructor(username, password) {
    this.username = username;
    this.password = password;
  }
  
  validate() {
    for (const prop in this) {
      if (this.hasOwnProperty(prop)) {
        Validator.validate(this, prop, this[prop]);
      }
    }
  }
}

// 测试
const user = new User('alice', 'password123');
user.validate(); // 成功

try {
  const invalidUser = new User('a', '123');
  invalidUser.validate(); // 抛出错误
} catch (error) {
  console.error(error.message);
}

四、类型推断与类型系统

类型推断实现
// 4.1 类型推断引擎
class TypeInferenceEngine {
  constructor() {
    this.typeRegistry = new Map();
    this.variableTypes = new Map();
  }
  
  // 推断变量类型
  inferVariableType(name, value) {
    let inferredType = this.inferValueType(value);
    
    // 检查是否已经声明过类型
    if (this.variableTypes.has(name)) {
      const declaredType = this.variableTypes.get(name);
      if (!this.isCompatible(inferredType, declaredType)) {
        throw new Error(`类型不匹配: 变量 ${name} 声明为 ${declaredType}, 但推断为 ${inferredType}`);
      }
      return declaredType;
    }
    
    // 记录推断的类型
    this.variableTypes.set(name, inferredType);
    return inferredType;
  }
  
  // 推断值类型
  inferValueType(value) {
    if (value === null) return 'null';
    if (value === undefined) return 'undefined';
    
    const type = typeof value;
    
    if (type === 'object') {
      if (Array.isArray(value)) {
        // 推断数组元素类型
        if (value.length === 0) return 'any[]';
        const elementType = this.inferValueType(value[0]);
        return `${elementType}[]`;
      }
      
      if (value instanceof Date) return 'Date';
      if (value instanceof RegExp) return 'RegExp';
      
      // 推断对象结构
      const structure = {};
      for (const key in value) {
        if (value.hasOwnProperty(key)) {
          structure[key] = this.inferValueType(value[key]);
        }
      }
      return JSON.stringify(structure);
    }
    
    return type;
  }
  
  // 检查类型兼容性
  isCompatible(type1, type2) {
    if (type1 === type2) return true;
    
    // 处理any类型
    if (type1 === 'any' || type2 === 'any') return true;
    
    // 处理数组类型
    if (type1.endsWith('[]') && type2.endsWith('[]')) {
      const elementType1 = type1.slice(0, -2);
      const elementType2 = type2.slice(0, -2);
      return this.isCompatible(elementType1, elementType2);
    }
    
    // 处理对象类型
    if (type1.startsWith('{') && type2.startsWith('{')) {
      try {
        const obj1 = JSON.parse(type1);
        const obj2 = JSON.parse(type2);
        
        for (const key in obj1) {
          if (obj1.hasOwnProperty(key)) {
            if (!obj2[key] || !this.isCompatible(obj1[key], obj2[key])) {
              return false;
            }
          }
        }
        return true;
      } catch {
        return false;
      }
    }
    
    return false;
  }
  
  // 推断函数返回类型
  inferFunctionReturn(func, args) {
    try {
      const result = func.apply(null, args);
      return this.inferValueType(result);
    } catch {
      return 'any';
    }
  }
}

// 使用示例
const engine = new TypeInferenceEngine();

const variables = {
  name: 'Alice',
  age: 30,
  scores: [95, 88, 92],
  profile: {
    email: 'alice@example.com',
    active: true
  }
};

for (const [name, value] of Object.entries(variables)) {
  const type = engine.inferVariableType(name, value);
  console.log(`${name}: ${type}`);
}

// 4.2 联合类型推断
function inferUnionType(values) {
  const types = new Set();
  
  for (const value of values) {
    const engine = new TypeInferenceEngine();
    types.add(engine.inferValueType(value));
  }
  
  return Array.from(types).join(' | ');
}

console.log(inferUnionType([1, 'hello', true])); // "number | string | boolean"

// 4.3 上下文类型推断
class ContextualTyping {
  static inferWithContext(value, context) {
    const engine = new TypeInferenceEngine();
    const valueType = engine.inferValueType(value);
    
    // 如果有上下文类型,检查兼容性
    if (context.expectedType) {
      if (!engine.isCompatible(valueType, context.expectedType)) {
        throw new Error(`上下文类型不匹配: 期望 ${context.expectedType}, 实际 ${valueType}`);
      }
      return context.expectedType;
    }
    
    return valueType;
  }
}

const context = { expectedType: 'string' };
console.log(ContextualTyping.inferWithContext('hello', context)); // "string"
// ContextualTyping.inferWithContext(123, context); // 抛出错误
高级类型系统
// 4.4 条件类型模拟
class ConditionalType {
  static extends(T, U) {
    // 简化实现:检查T是否可以赋值给U
    return T === U || T === 'any';
  }
  
  static extract(T, U) {
    // 模拟 Extract<T, U>
    if (this.extends(T, U)) {
      return T;
    }
    return 'never';
  }
  
  static exclude(T, U) {
    // 模拟 Exclude<T, U>
    if (this.extends(T, U)) {
      return 'never';
    }
    return T;
  }
  
  static nonNullable(T) {
    // 模拟 NonNullable<T>
    if (T === 'null' || T === 'undefined') {
      return 'never';
    }
    return T;
  }
  
  static returnType(func) {
    // 模拟 ReturnType<T>
    try {
      const result = func();
      const engine = new TypeInferenceEngine();
      return engine.inferValueType(result);
    } catch {
      return 'any';
    }
  }
  
  static parameters(func) {
    // 模拟 Parameters<T>
    const funcStr = func.toString();
    const paramMatch = funcStr.match(/\(([^)]*)\)/);
    
    if (paramMatch) {
      const params = paramMatch[1].split(',').map(p => p.trim());
      return params.map(param => {
        const [name, type] = param.split(':').map(s => s.trim());
        return type || 'any';
      });
    }
    
    return [];
  }
}

// 使用示例
console.log(ConditionalType.extends('string', 'string')); // true
console.log(ConditionalType.extract('string | number', 'string')); // "string"
console.log(ConditionalType.exclude('string | number', 'string')); // "number"
console.log(ConditionalType.nonNullable('string | null')); // "string"

function sampleFunc(x, y) {
  return x + y;
}

console.log(ConditionalType.returnType(() => sampleFunc(1, 2))); // "number"
console.log(ConditionalType.parameters(sampleFunc)); // ["any", "any"]

// 4.5 映射类型模拟
class MappedType {
  static partial(T) {
    // 模拟 Partial<T>
    if (typeof T === 'string' && T.startsWith('{')) {
      try {
        const obj = JSON.parse(T);
        const result = {};
        for (const key in obj) {
          result[key] = obj[key] + '?'; // 添加可选标记
        }
        return JSON.stringify(result);
      } catch {
        return T;
      }
    }
    return T;
  }
  
  static required(T) {
    // 模拟 Required<T>
    if (typeof T === 'string' && T.startsWith('{')) {
      try {
        const obj = JSON.parse(T);
        const result = {};
        for (const key in obj) {
          const type = obj[key].endsWith('?') ? obj[key].slice(0, -1) : obj[key];
          result[key] = type;
        }
        return JSON.stringify(result);
      } catch {
        return T;
      }
    }
    return T;
  }
  
  static readonly(T) {
    // 模拟 Readonly<T>
    if (typeof T === 'string' && T.startsWith('{')) {
      try {
        const obj = JSON.parse(T);
        const result = {};
        for (const key in obj) {
          result[key] = 'readonly ' + obj[key];
        }
        return JSON.stringify(result);
      } catch {
        return T;
      }
    }
    return T;
  }
  
  static record(K, T) {
    // 模拟 Record<K, T>
    const keys = K.split(' | ');
    const result = {};
    keys.forEach(key => {
      const cleanKey = key.replace(/['"]/g, '');
      result[cleanKey] = T;
    });
    return JSON.stringify(result);
  }
}

// 使用示例
const userType = JSON.stringify({
  name: 'string',
  age: 'number?',
  email: 'string'
});

console.log('Partial:', MappedType.partial(userType));
console.log('Required:', MappedType.required(userType));
console.log('Readonly:', MappedType.readonly(userType));
console.log('Record:', MappedType.record('"id" | "name"', 'string'));

五、模块与命名空间

TypeScript模块系统
// 5.1 模块加载器模拟
class TypeScriptModuleLoader {
  constructor() {
    this.modules = new Map();
    this.exports = new Map();
    this.moduleCache = new Map();
  }
  
  // 定义模块
  define(moduleId, dependencies, factory) {
    this.modules.set(moduleId, {
      dependencies,
      factory,
      exports: {},
      resolved: false
    });
  }
  
  // 解析模块
  async require(moduleId) {
    if (this.moduleCache.has(moduleId)) {
      return this.moduleCache.get(moduleId);
    }
    
    const module = this.modules.get(moduleId);
    if (!module) {
      // 尝试作为外部模块加载
      return this.loadExternalModule(moduleId);
    }
    
    // 解析依赖
    const depPromises = module.dependencies.map(dep => {
      if (dep === 'exports') return {};
      if (dep === 'require') return this.require.bind(this);
      if (dep === 'module') return { id: moduleId, exports: {} };
      return this.require(dep);
    });
    
    const dependencies = await Promise.all(depPromises);
    
    // 执行工厂函数
    const exports = module.factory.apply(null, dependencies);
    
    // 缓存结果
    module.exports = exports || {};
    module.resolved = true;
    this.moduleCache.set(moduleId, module.exports);
    
    return module.exports;
  }
  
  // 加载外部模块(模拟)
  async loadExternalModule(moduleId) {
    console.log(`加载外部模块: ${moduleId}`);
    
    // 模拟动态导入
    if (moduleId.startsWith('.')) {
      // 相对路径
      const mockModule = await this.mockLoadModule(moduleId);
      return mockModule;
    }
    
    // 假设是npm包
    return {};
  }
  
  async mockLoadModule(path) {
    // 模拟模块内容
    const mockModules = {
      './math': {
        add: (a, b) => a + b,
        subtract: (a, b) => a - b
      },
      './utils': {
        format: str => str.toUpperCase(),
        parse: str => JSON.parse(str)
      }
    };
    
    await new Promise(resolve => setTimeout(resolve, 100)); // 模拟延迟
    
    return mockModules[path] || {};
  }
  
  // 编译TypeScript模块
  compileModule(source) {
    // 移除类型注解
    const jsCode = source
      .replace(/:\s*\w+(?:<[^>]*>)?(?=\s*[;,=){}])/g, '') // 移除类型注解
      .replace(/export\s+(?:default\s+)?/g, '') // 简化导出
      .replace(/import\s+.*?from\s+['"](.+)['"]/g, '// 导入: $1'); // 注释化导入
    
    return jsCode;
  }
}

// 使用示例
const loader = new TypeScriptModuleLoader();

// 定义TypeScript模块
const mathSource = `
export function add(a: number, b: number): number {
  return a + b;
}

export function subtract(a: number, b: number): number {
  return a - b;
}

export default class Calculator {
  multiply(a: number, b: number): number {
    return a * b;
  }
}
`;

const appSource = `
import { add } from './math';
import Calculator from './math';

export function calculate(): number {
  const calc = new Calculator();
  return add(10, calc.multiply(2, 3));
}
`;

// 编译模块
const mathJS = loader.compileModule(mathSource);
const appJS = loader.compileModule(appSource);

console.log('编译后的math模块:');
console.log(mathJS);

console.log('编译后的app模块:');
console.log(appJS);

// 5.2 命名空间实现
class NamespaceManager {
  static namespaces = new Map();
  
  static create(namespace, contents) {
    if (!this.namespaces.has(namespace)) {
      this.namespaces.set(namespace, {});
    }
    
    const ns = this.namespaces.get(namespace);
    Object.assign(ns, contents);
    
    // 暴露到全局
    const parts = namespace.split('.');
    let current = globalThis;
    
    for (let i = 0; i < parts.length; i++) {
      const part = parts[i];
      if (i === parts.length - 1) {
        current[part] = ns;
      } else {
        current[part] = current[part] || {};
        current = current[part];
      }
    }
    
    return ns;
  }
  
  static get(namespace) {
    return this.namespaces.get(namespace);
  }
  
  static export(namespace, exports) {
    const ns = this.get(namespace) || this.create(namespace, {});
    Object.assign(ns, exports);
  }
}

// 使用命名空间
NamespaceManager.create('MyApp.Math', {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  constants: {
    PI: 3.14159,
    E: 2.71828
  }
});

NamespaceManager.create('MyApp.Utils', {
  format: str => str.toUpperCase(),
  validate: obj => obj != null
});

// 访问命名空间
console.log(MyApp.Math.add(2, 3)); // 5
console.log(MyApp.Utils.format('hello')); // "HELLO"

六、工程化与构建工具集成

TypeScript配置与编译
// 6.1 TypeScript配置解析器
class TSConfigParser {
  static parse(config) {
    const defaults = {
      compilerOptions: {
        target: 'es5',
        module: 'commonjs',
        strict: false,
        esModuleInterop: true,
        skipLibCheck: true,
        forceConsistentCasingInFileNames: true
      },
      include: ['src/**/*'],
      exclude: ['node_modules', 'dist']
    };
    
    return this.deepMerge(defaults, config);
  }
  
  static deepMerge(target, source) {
    for (const key in source) {
      if (source.hasOwnProperty(key)) {
        if (this.isObject(source[key]) && this.isObject(target[key])) {
          this.deepMerge(target[key], source[key]);
        } else {
          target[key] = source[key];
        }
      }
    }
    return target;
  }
  
  static isObject(item) {
    return item && typeof item === 'object' && !Array.isArray(item);
  }
  
  static generateCompilerCommand(config) {
    const options = config.compilerOptions;
    const args = [];
    
    for (const [key, value] of Object.entries(options)) {
      const argName = `--${key}`;
      
      if (typeof value === 'boolean') {
        if (value) args.push(argName);
      } else if (typeof value === 'string') {
        args.push(`${argName} ${value}`);
      } else if (Array.isArray(value)) {
        args.push(`${argName} ${value.join(',')}`);
      }
    }
    
    return `tsc ${args.join(' ')}`;
  }
}

// 示例配置
const tsconfig = {
  compilerOptions: {
    target: 'es2020',
    module: 'esnext',
    lib: ['es2020', 'dom'],
    outDir: './dist',
    rootDir: './src',
    strict: true,
    moduleResolution: 'node',
    baseUrl: '.',
    paths: {
      '@/*': ['src/*']
    },
    allowSyntheticDefaultImports: true
  },
  include: ['src/**/*.ts', 'src/**/*.tsx'],
  exclude: ['node_modules']
};

const parsedConfig = TSConfigParser.parse(tsconfig);
console.log('解析后的配置:', parsedConfig);
console.log('编译器命令:', TSConfigParser.generateCompilerCommand(parsedConfig));

// 6.2 构建管道模拟
class TypeScriptBuildPipeline {
  constructor(config) {
    this.config = config;
    this.steps = [];
  }
  
  addStep(name, processor) {
    this.steps.push({ name, processor });
    return this;
  }
  
  async build(sourceFiles) {
    console.log('开始构建TypeScript项目...');
    
    let processedFiles = sourceFiles;
    
    for (const step of this.steps) {
      console.log(`执行步骤: ${step.name}`);
      
      if (Array.isArray(processedFiles)) {
        const results = await Promise.all(
          processedFiles.map(file => step.processor(file, this.config))
        );
        processedFiles = results;
      } else {
        processedFiles = await step.processor(processedFiles, this.config);
      }
    }
    
    console.log('构建完成!');
    return processedFiles;
  }
}

// 构建步骤处理器
const processors = {
  // 类型检查
  typeCheck: async (file, config) => {
    console.log(`类型检查: ${file.name}`);
    // 模拟类型检查
    if (file.name.includes('error')) {
      throw new Error(`类型错误: ${file.name}`);
    }
    return file;
  },
  
  // 编译
  compile: async (file, config) => {
    console.log(`编译: ${file.name}`);
    
    // 模拟编译过程
    const jsCode = file.content
      .replace(/:\s*\w+/g, '') // 移除类型注解
      .replace(/interface\s+\w+\s*\{[^}]+\}/g, '') // 移除接口
      .replace(/export\s+type\s+\w+/g, '') // 移除类型导出
      .trim();
    
    return {
      ...file,
      content: jsCode,
      extension: '.js'
    };
  },
  
  // 打包
  bundle: async (files, config) => {
    console.log('打包文件...');
    
    const bundleContent = files
      .map(file => `// ${file.name}\n${file.content}`)
      .join('\n\n');
    
    return {
      name: 'bundle.js',
      content: bundleContent,
      size: bundleContent.length
    };
  },
  
  // 压缩
  minify: async (file, config) => {
    console.log('压缩代码...');
    
    // 简单压缩:移除空格和注释
    const minified = file.content
      .replace(/\/\/.*$/gm, '')
      .replace(/\/\*[\s\S]*?\*\//g, '')
      .replace(/\s+/g, ' ')
      .trim();
    
    return {
      ...file,
      content: minified,
      size: minified.length
    };
  }
};

// 模拟源文件
const sourceFiles = [
  {
    name: 'math.ts',
    content: `
      export function add(a: number, b: number): number {
        return a + b;
      }
      
      export function subtract(a: number, b: number): number {
        return a - b;
      }
    `
  },
  {
    name: 'app.ts',
    content: `
      import { add } from './math';
      
      export function calculate(): number {
        return add(1, 2);
      }
    `
  }
];

// 创建构建管道
const pipeline = new TypeScriptBuildPipeline(tsconfig)
  .addStep('类型检查', processors.typeCheck)
  .addStep('编译', processors.compile)
  .addStep('打包', processors.bundle)
  .addStep('压缩', processors.minify);

// 执行构建
pipeline.build(sourceFiles).then(result => {
  console.log('构建结果:', result);
});

七、实战应用: 在JavaScript项目中引入TypeScript

渐进式迁移策略
// 7.1 混合项目结构管理
class HybridProjectManager {
  constructor(options = {}) {
    this.options = {
      tsDir: 'src/ts',
      jsDir: 'src/js',
      buildDir: 'dist',
      allowJS: true,
      checkJS: true,
      ...options
    };
    
    this.fileRegistry = new Map();
  }
  
  // 注册文件
  registerFile(path, type) {
    this.fileRegistry.set(path, {
      type,
      dependencies: [],
      errors: []
    });
    
    // 自动检测文件类型
    if (path.endsWith('.ts') || path.endsWith('.tsx')) {
      this.fileRegistry.get(path).type = 'typescript';
    } else if (path.endsWith('.js') || path.endsWith('.jsx')) {
      this.fileRegistry.get(path).type = 'javascript';
    }
  }
  
  // 分析依赖关系
  analyzeDependencies(path) {
    const file = this.fileRegistry.get(path);
    if (!file) return;
    
    const content = this.readFile(path);
    const dependencies = [];
    
    // 解析导入语句
    const importRegex = /import\s+(?:.*?from\s+)?['"]([^'"]+)['"]/g;
    let match;
    
    while ((match = importRegex.exec(content)) !== null) {
      dependencies.push(match[1]);
    }
    
    file.dependencies = dependencies;
  }
  
  // 检查混合项目问题
  checkHybridIssues() {
    const issues = [];
    
    for (const [path, file] of this.fileRegistry) {
      // 检查TypeScript文件是否导入了未定义的JavaScript模块
      if (file.type === 'typescript') {
        for (const dep of file.dependencies) {
          if (dep.endsWith('.js')) {
            const depPath = this.resolvePath(path, dep);
            const depFile = this.fileRegistry.get(depPath);
            
            if (depFile && depFile.type === 'javascript') {
              issues.push({
                type: 'ts-imports-js',
                message: `TypeScript文件 ${path} 导入了JavaScript文件 ${depPath}`,
                severity: 'warning',
                suggestion: '考虑将JavaScript文件迁移为TypeScript或添加类型声明'
              });
            }
          }
        }
      }
      
      // 检查JavaScript文件是否缺少JSDoc注释
      if (file.type === 'javascript' && this.options.checkJS) {
        const content = this.readFile(path);
        const hasJSDoc = /\/\*\*\s*\n(?:[^*]|\*(?!\/))*\*\//.test(content);
        
        if (!hasJSDoc) {
          issues.push({
            type: 'missing-jsdoc',
            message: `JavaScript文件 ${path} 缺少JSDoc注释`,
            severity: 'info',
            suggestion: '添加JSDoc注释以提供类型信息'
          });
        }
      }
    }
    
    return issues;
  }
  
  // 生成迁移建议
  generateMigrationPlan() {
    const plan = {
      phase1: [], // 立即迁移的文件
      phase2: [], // 可以稍后迁移的文件
      declarations: [] // 需要创建的类型声明文件
    };
    
    for (const [path, file] of this.fileRegistry) {
      if (file.type === 'javascript') {
        // 分析复杂性
        const complexity = this.assessComplexity(path);
        
        if (complexity <= 2) {
          plan.phase1.push(path);
        } else {
          plan.phase2.push(path);
        }
        
        // 检查是否需要声明文件
        if (file.dependencies.some(dep => dep.startsWith('@types/'))) {
          plan.declarations.push(path);
        }
      }
    }
    
    return plan;
  }
  
  // 评估文件复杂性
  assessComplexity(path) {
    const content = this.readFile(path);
    let score = 0;
    
    // 基于行数
    const lines = content.split('\n').length;
    if (lines > 100) score += 2;
    else if (lines > 50) score += 1;
    
    // 基于函数数量
    const functionCount = (content.match(/function\s+\w+|\b\w+\s*=/g) || []).length;
    score += Math.min(Math.floor(functionCount / 5), 3);
    
    // 基于外部依赖
    const importCount = (content.match(/import\s+|require\(/g) || []).length;
    score += Math.min(importCount, 2);
    
    return score;
  }
  
  // 工具方法
  readFile(path) {
    // 模拟文件读取
    return '模拟文件内容';
  }
  
  resolvePath(base, relative) {
    // 简化路径解析
    return relative;
  }
}

// 使用示例
const manager = new HybridProjectManager({
  tsDir: 'src/ts',
  jsDir: 'src/js',
  checkJS: true
});

// 注册文件
manager.registerFile('src/js/user.js', 'javascript');
manager.registerFile('src/ts/auth.ts', 'typescript');
manager.registerFile('src/js/utils.js', 'javascript');

// 分析问题
const issues = manager.checkHybridIssues();
console.log('发现的问题:', issues);

// 生成迁移计划
const migrationPlan = manager.generateMigrationPlan();
console.log('迁移计划:', migrationPlan);

// 7.2 类型声明生成器
class TypeDeclarationGenerator {
  static generateFromJS(jsCode, options = {}) {
    const declarations = [];
    
    // 解析函数
    const functionRegex = /function\s+(\w+)\s*\(([^)]*)\)/g;
    let match;
    
    while ((match = functionRegex.exec(jsCode)) !== null) {
      const [, funcName, params] = match;
      const paramTypes = this.inferParamTypes(params);
      const returnType = this.inferReturnType(jsCode, funcName);
      
      declarations.push(
        `declare function ${funcName}(${paramTypes}): ${returnType};`
      );
    }
    
    // 解析类
    const classRegex = /class\s+(\w+)/g;
    while ((match = classRegex.exec(jsCode)) !== null) {
      const [, className] = match;
      const classDeclaration = this.generateClassDeclaration(jsCode, className);
      declarations.push(classDeclaration);
    }
    
    // 解析变量导出
    const exportRegex = /exports\.(\w+)\s*=/g;
    while ((match = exportRegex.exec(jsCode)) !== null) {
      const [, exportName] = match;
      const type = this.inferVariableType(jsCode, exportName);
      declarations.push(`declare const ${exportName}: ${type};`);
    }
    
    return declarations.join('\n');
  }
  
  static inferParamTypes(paramsStr) {
    const params = paramsStr.split(',').map(p => p.trim()).filter(p => p);
    
    return params.map((param, index) => {
      const [name, defaultValue] = param.split('=').map(s => s.trim());
      let type = 'any';
      
      if (defaultValue) {
        if (defaultValue.includes("'") || defaultValue.includes('"')) {
          type = 'string';
        } else if (defaultValue === 'true' || defaultValue === 'false') {
          type = 'boolean';
        } else if (!isNaN(parseFloat(defaultValue))) {
          type = 'number';
        } else if (defaultValue === '[]') {
          type = 'any[]';
        } else if (defaultValue === '{}') {
          type = 'object';
        }
      }
      
      return `${name}: ${type}`;
    }).join(', ');
  }
  
  static inferReturnType(jsCode, funcName) {
    // 简化实现:检查return语句
    const returnRegex = new RegExp(`function\\s+${funcName}[^{]*{([^}]*return[^;]+;)?`, 'i');
    const match = jsCode.match(returnRegex);
    
    if (match && match[1]) {
      const returnExpr = match[1].replace('return', '').trim();
      
      if (returnExpr.includes("'") || returnExpr.includes('"')) {
        return 'string';
      } else if (returnExpr === 'true' || returnExpr === 'false') {
        return 'boolean';
      } else if (!isNaN(parseFloat(returnExpr))) {
        return 'number';
      }
    }
    
    return 'any';
  }
  
  static generateClassDeclaration(jsCode, className) {
    const declaration = [`declare class ${className} {`];
    
    // 提取构造函数
    const constructorRegex = new RegExp(`constructor\\(([^)]*)\\)`, 'g');
    const constructorMatch = constructorRegex.exec(jsCode);
    
    if (constructorMatch) {
      const params = constructorMatch[1];
      const paramTypes = this.inferParamTypes(params);
      declaration.push(`  constructor(${paramTypes});`);
    }
    
    // 提取方法
    const methodRegex = new RegExp(`${className}\\.prototype\\.(\\w+)\\s*=\\s*function`, 'g');
    while ((match = methodRegex.exec(jsCode)) !== null) {
      const [, methodName] = match;
      declaration.push(`  ${methodName}(): any;`);
    }
    
    declaration.push('}');
    return declaration.join('\n');
  }
  
  static inferVariableType(jsCode, varName) {
    const regex = new RegExp(`${varName}\\s*=\\s*([^;]+)`, 'g');
    const match = regex.exec(jsCode);
    
    if (match) {
      const value = match[1].trim();
      
      if (value.startsWith('[')) return 'any[]';
      if (value.startsWith('{')) return 'object';
      if (value.includes("'") || value.includes('"')) return 'string';
      if (value === 'true' || value === 'false') return 'boolean';
      if (!isNaN(parseFloat(value))) return 'number';
    }
    
    return 'any';
  }
}

// 生成类型声明示例
const jsCode = `
function greet(name) {
  return 'Hello, ' + name;
}

class User {
  constructor(name, age = 18) {
    this.name = name;
    this.age = age;
  }
  
  sayHello() {
    return greet(this.name);
  }
}

exports.greet = greet;
exports.User = User;
exports.VERSION = '1.0.0';
`;

const declarations = TypeDeclarationGenerator.generateFromJS(jsCode);
console.log('生成的类型声明:');
console.log(declarations);

八、性能优化与最佳实践

TypeScript编译优化
// 8.1 增量编译
class IncrementalCompiler {
  constructor() {
    this.fileCache = new Map();
    this.dependencyGraph = new Map();
    this.lastCompileTime = Date.now();
  }
  
  // 检查文件是否需要重新编译
  needsRecompile(filePath, content) {
    const cached = this.fileCache.get(filePath);
    
    if (!cached) {
      return true; // 新文件
    }
    
    if (cached.content !== content) {
      return true; // 内容改变
    }
    
    // 检查依赖是否改变
    const dependencies = this.dependencyGraph.get(filePath) || [];
    for (const dep of dependencies) {
      const depCached = this.fileCache.get(dep);
      if (!depCached || depCached.timestamp > cached.timestamp) {
        return true; // 依赖已更新
      }
    }
    
    return false; // 无需重新编译
  }
  
  // 增量编译
  incrementalCompile(files) {
    const toCompile = [];
    const startTime = Date.now();
    
    for (const file of files) {
      if (this.needsRecompile(file.path, file.content)) {
        toCompile.push(file);
      }
    }
    
    console.log(`需要重新编译 ${toCompile.length} 个文件`);
    
    if (toCompile.length === 0) {
      console.log('没有文件需要重新编译,使用缓存');
      return this.getCachedOutput();
    }
    
    // 执行编译
    const results = this.compileFiles(toCompile);
    
    // 更新缓存
    for (const file of toCompile) {
      this.fileCache.set(file.path, {
        content: file.content,
        timestamp: startTime,
        output: results[file.path]
      });
    }
    
    this.lastCompileTime = startTime;
    
    return results;
  }
  
  compileFiles(files) {
    // 简化编译过程
    const results = {};
    
    for (const file of files) {
      results[file.path] = file.content.replace(/:\s*\w+/g, '');
    }
    
    return results;
  }
  
  getCachedOutput() {
    const output = {};
    
    for (const [path, cached] of this.fileCache) {
      output[path] = cached.output;
    }
    
    return output;
  }
}

// 8.2 项目引用优化
class ProjectReferenceOptimizer {
  static optimize(config, projects) {
    const optimized = { ...config };
    
    // 分析项目依赖
    const dependencies = this.analyzeDependencies(projects);
    
    // 设置项目引用
    optimized.compilerOptions = {
      ...optimized.compilerOptions,
      composite: true,
      declaration: true,
      declarationMap: true,
      incremental: true
    };
    
    optimized.references = dependencies.map(dep => ({ path: dep }));
    
    return optimized;
  }
  
  static analyzeDependencies(projects) {
    const graph = new Map();
    
    // 构建依赖图
    for (const project of projects) {
      const deps = this.extractImports(project.source);
      graph.set(project.name, deps);
    }
    
    // 拓扑排序
    return this.topologicalSort(graph);
  }
  
  static extractImports(source) {
    const imports = [];
    const importRegex = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
    let match;
    
    while ((match = importRegex.exec(source)) !== null) {
      imports.push(match[1]);
    }
    
    return imports;
  }
  
  static topologicalSort(graph) {
    const visited = new Set();
    const result = [];
    
    const visit = (node) => {
      if (visited.has(node)) return;
      visited.add(node);
      
      const dependencies = graph.get(node) || [];
      for (const dep of dependencies) {
        visit(dep);
      }
      
      result.push(node);
    };
    
    for (const node of graph.keys()) {
      visit(node);
    }
    
    return result;
  }
}

// 8.3 内存优化
class TypeScriptMemoryOptimizer {
  constructor() {
    this.memoryCache = new Map();
    this.maxCacheSize = 100;
  }
  
  // 编译结果缓存
  cacheCompilationResult(filePath, ast, diagnostics, output) {
    const cacheEntry = {
      ast,
      diagnostics,
      output,
      timestamp: Date.now(),
      size: this.calculateSize(ast) + this.calculateSize(output)
    };
    
    this.memoryCache.set(filePath, cacheEntry);
    this.cleanupCache();
  }
  
  // 获取缓存结果
  getCachedResult(filePath) {
    const cached = this.memoryCache.get(filePath);
    if (cached) {
      // 更新访问时间
      cached.timestamp = Date.now();
      return cached;
    }
    return null;
  }
  
  // 清理缓存
  cleanupCache() {
    if (this.memoryCache.size <= this.maxCacheSize) {
      return;
    }
    
    // LRU缓存清理
    const entries = Array.from(this.memoryCache.entries());
    entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
    
    const toRemove = entries.slice(0, entries.length - this.maxCacheSize);
    
    for (const [key] of toRemove) {
      this.memoryCache.delete(key);
    }
  }
  
  // 计算对象大小
  calculateSize(obj) {
    const str = JSON.stringify(obj);
    return str.length * 2; // 粗略估计(UTF-16)
  }
  
  // AST节点缓存池
  createASTNodePool() {
    const pool = new Map();
    
    return {
      getNode(type, props) {
        const key = `${type}:${JSON.stringify(props)}`;
        
        if (pool.has(key)) {
          return pool.get(key);
        }
        
        const node = { type, ...props };
        pool.set(key, node);
        
        return node;
      },
      
      clear() {
        pool.clear();
      },
      
      get size() {
        return pool.size;
      }
    };
  }
}
TypeScript最佳实践
// 8.4 代码组织最佳实践
class TypeScriptBestPractices {
  static structureProject(projectType) {
    const structures = {
      library: {
        src: [
          'index.ts', // 主入口
          'types/',   // 类型定义
          'utils/',   // 工具函数
          'core/',    // 核心逻辑
          'tests/'    // 测试文件
        ],
        config: [
          'tsconfig.json',
          'tsconfig.build.json',
          'package.json',
          '.eslintrc.js'
        ],
        docs: 'README.md'
      },
      
      application: {
        src: [
          'main.ts',          // 应用入口
          'components/',      // 组件
          'services/',        // 服务层
          'models/',          // 数据模型
          'utils/',           // 工具函数
          'styles/',          // 样式文件
          'assets/',          // 静态资源
          'tests/'            // 测试
        ],
        config: [
          'tsconfig.json',
          'tsconfig.app.json',
          'package.json',
          'webpack.config.js'
        ],
        public: 'index.html'
      },
      
      node: {
        src: [
          'index.ts',         // 应用入口
          'controllers/',     // 控制器
          'services/',        // 服务层
          'models/',          // 数据模型
          'middleware/',      // 中间件
          'routes/',          // 路由
          'utils/',           // 工具函数
          'tests/'            // 测试
        ],
        config: [
          'tsconfig.json',
          'package.json',
          '.env',
          'dockerfile'
        ]
      }
    };
    
    return structures[projectType] || structures.library;
  }
  
  static namingConventions = {
    interfaces: {
      rule: '使用 PascalCase,以 I 开头',
      examples: ['IUser', 'IProductService', 'IRepository']
    },
    
    types: {
      rule: '使用 PascalCase',
      examples: ['UserData', 'ApiResponse', 'ConfigOptions']
    },
    
    classes: {
      rule: '使用 PascalCase',
      examples: ['UserService', 'DatabaseConnection', 'HttpClient']
    },
    
    variables: {
      rule: '使用 camelCase',
      examples: ['userName', 'productList', 'isLoading']
    },
    
    constants: {
      rule: '使用 UPPER_SNAKE_CASE',
      examples: ['MAX_RETRY_COUNT', 'API_BASE_URL', 'DEFAULT_TIMEOUT']
    },
    
    generics: {
      rule: '使用单个大写字母,T 开头',
      examples: ['T', 'K', 'V', 'TKey', 'TValue']
    }
  };
  
  static createTSConfig(projectType, options = {}) {
    const baseConfig = {
      compilerOptions: {
        target: 'es2020',
        module: 'commonjs',
        lib: ['es2020'],
        outDir: './dist',
        rootDir: './src',
        strict: true,
        esModuleInterop: true,
        skipLibCheck: true,
        forceConsistentCasingInFileNames: true
      }
    };
    
    const typeSpecific = {
      library: {
        compilerOptions: {
          declaration: true,
          declarationMap: true,
          sourceMap: true
        }
      },
      
      application: {
        compilerOptions: {
          jsx: 'react-jsx',
          moduleResolution: 'node',
          allowSyntheticDefaultImports: true
        }
      },
      
      node: {
        compilerOptions: {
          module: 'commonjs',
          target: 'es2020',
          lib: ['es2020']
        }
      }
    };
    
    return this.deepMerge(
      baseConfig,
      typeSpecific[projectType] || {},
      { compilerOptions: options }
    );
  }
  
  static deepMerge(...objects) {
    return objects.reduce((acc, obj) => {
      for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
          if (this.isObject(obj[key]) && this.isObject(acc[key])) {
            acc[key] = this.deepMerge(acc[key], obj[key]);
          } else {
            acc[key] = obj[key];
          }
        }
      }
      return acc;
    }, {});
  }
  
  static isObject(item) {
    return item && typeof item === 'object' && !Array.isArray(item);
  }
  
  // 错误处理最佳实践
  static errorHandlingPatterns = {
    // 使用Result类型处理错误
    Result: `
type Result<T, E = Error> = 
  | { success: true; data: T }
  | { success: false; error: E };

function safeOperation(): Result<string> {
  try {
    const result = riskyOperation();
    return { success: true, data: result };
  } catch (error) {
    return { success: false, error: error as Error };
  }
}
    `,
    
    // 自定义错误类
    CustomError: `
class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number = 500
  ) {
    super(message);
    this.name = 'AppError';
  }
}

class ValidationError extends AppError {
  constructor(message: string) {
    super(message, 'VALIDATION_ERROR', 400);
    this.name = 'ValidationError';
  }
}
    `,
    
    // 错误边界
    ErrorBoundary: `
class ErrorBoundary {
  private errorHandlers = new Map<string, (error: Error) => void>();
  
  register(component: string, handler: (error: Error) => void) {
    this.errorHandlers.set(component, handler);
  }
  
  handle(component: string, error: Error) {
    const handler = this.errorHandlers.get(component);
    if (handler) {
      handler(error);
    } else {
      console.error(\`未处理的错误 (\${component}):\`, error);
    }
  }
}
    `
  };
}

// 使用最佳实践
console.log('库项目结构:', TypeScriptBestPractices.structureProject('library'));
console.log('命名约定:', TypeScriptBestPractices.namingConventions);

const libConfig = TypeScriptBestPractices.createTSConfig('library', {
  outDir: './lib',
  declarationDir: './types'
});

console.log('库TS配置:', JSON.stringify(libConfig, null, 2));

总结

TypeScript通过强大的类型系统、接口泛型、装饰器等特性,极大地提升了JavaScript代码的质量和开发体验。本文深入探讨了TypeScript的核心实现原理和高级特性,并提供了实际可用的代码示例。

核心要点总结:

  1. 类型系统: TypeScript通过编译时类型检查提供安全保障,运行时也可通过代理模式实现类型验证
  2. 接口与泛型: 通过设计模式模拟接口实现,泛型提供代码复用性和类型安全性
  3. 装饰器: 元编程的强大工具,实现依赖注入、验证、日志等横切关注点
  4. 类型推断: 自动推断变量类型,减少冗余的类型注解
  5. 模块与工程化: 完善的模块系统支持,与构建工具深度集成
  6. 最佳实践: 合理的项目结构、命名约定和错误处理策略

TypeScript不仅是JavaScript的类型超集,更是一种工程化实践。它将静态类型语言的严谨性与JavaScript的灵活性完美结合,成为现代前端开发不可或缺的工具。随着TypeScript的持续发展,其类型系统将变得更加智能和强大,为构建大型、可维护的应用程序提供坚实保障。

在实际项目中,应根据团队规模、项目复杂度和技术栈选择合适的TypeScript特性,平衡类型安全性和开发效率。通过渐进式迁移策略,即使现有JavaScript项目也能平滑过渡到TypeScript,享受类型系统带来的诸多好处。

告别杂乱数字:用 Intl.NumberFormat 打造全球友好的前端体验

作者 CC码码
2025年12月22日 13:04

大家好,我是CC,在这里欢迎大家的到来~

开场

书接上文,Intl 下的 Segmenter 对象可以实现对文本的分割,Collator 对象可以处理字符串的比较,除此之外,还有数字格式化、日期格式化等其他功能。

这篇文章先来看看数字格式化,现在来理论加实践一下。

数字格式化

Intl.NumberFormat使数字在特定语言环境下格式化。

配置项

为了方便阅读,属性列表根据用途划分为多个部分,包括区域选项、样式选项、数字选项和其他选项。

区域选项

  • localeMatcher
    • 使用的区域匹配算法,可能的值包括:
    • 默认值为 best fit,还有 lookup
  • numberingSystem
    • 数字格式化的数字系统,像阿拉伯数字 arab、简体中文数字 hans、无衬线数字 mathsans
    • 默认值取决于区域
    • 同 locales 的 Unicode 扩展键 nu 设置,但优先级高于他

样式选项

  • style
    • 使用的格式化样式,可选的值包括:
    • decimal: 普通数字格式化
    • currency: 货币格式化
    • percent: 百分比格式化
    • unit: 单位格式化
    • 默认值是 decimal
  • currency
    • 货币格式化中使用的货币,像美元 USD、欧元 EUR 和人民币 CNY。
    • 没有默认值,style 为 currency 时必须提供,内容会被转换为大写。
  • currencyDisplay
    • 货币格式化中如何显示货币,可选的值包括:
    • code: 使用 ISO 货币代码
    • symbol: 使用本地化货币符号
    • narrowSymbol: 使用窄格式符号,像 100而不是US100 而不是 US100
    • name: 使用本地化货币名称,像 dollar
  • currencySign
    • 使用括号将数字括起来,而不是添加负号,可选的值包括:
    • standard: 默认值
    • accounting: 会计
  • unit
    • 格式化的单位
    • style 为 unit 时必填
  • unitDisplay
    • unit 格式化时使用的格式化风格,可选的值包括:
    • short: 默认值,例如 16 l
    • narrow: 例如 16l
    • long: 例如 16 litres

数字选项,由 Intl.PluralRules 支持

  • minimumIntegerDigits
    • 最小整数位数,默认值为 1,范围是 1~21
    • 若实际整数位数不足会在左侧用 0 补足,比如对于数字 5 该值设置为 3 则显示为“005”
  • minimumFractionDigits
    • 小数部分的最小位数,范围是 0~100
    • 若小数位数不足时会在右侧补 0,超过时会按四舍五入截断
    • 默认值对于普通数字和百分比是 0,对于 currency 是 2(ISO 4217 标准小数位数)
  • maximumFractionDigits
    • 小数部分的最大位数,范围是 0~100
    • 若小数位数不足时会在右侧补 0,超过时会按四舍五入截断
    • 默认值对于普通数字和百分比是 3,对于 currency 是 2(ISO 4217 标准小数位数)
  • minimumSignificanntDigits
    • 最小有效数字,默认值为 1。范围是 1~21。
    • 优先级高于 minimumFractionDigits
  • maximumSignificanntDigits
    • 最大有效数字,默认值为 21。范围是 1~21。
    • 优先级高于 maximumFractionDigits
  • roundingPriority
    • 当同时使用 FractionDigits 和 SignificantDigits 时指定如何解决四舍五入冲突,可选的值包括:
    • auto: 默认值,使用有效数字属性
    • morePrecision: 使用精度更高的属性
    • lessPrecision: 使用精度更低的属性
    • auto 属性会在 natation 为 compact 时且未设置任何四个 FractionDigits/SignificantDigits 时会被设置为 morePrecision
    • 除 auto 属性以外的值会根据 maximumSignificanntDigits 和 maximumFractionDigits 计算出更高精度,忽略最小小数位和有效数字位
  • roundingIncrement
    • 相对于计算出的舍入单位的舍入增量
    • 默认值为 1,其他值包括 1、2、5、10、20、25、50、100、200、250、500、1000、2000、5000
    • 不能与有效数字位舍入或任何 roundingPriority(除了 auto) 混合使用
  • roundingMode
    • 对小数进行舍入,可选的值包括:
    • ceil: 向正无穷舍入,正数向上,负数“向正”
    • floor: 向负无穷舍入,正数向下,负数“向负”
    • expand: 四舍五入远离 0,绝对值增大
    • trunc: 四舍五入朝向 0,绝对值减小
    • halfCeil: 趋向于正无穷舍入,包括半值
    • halfFloor: 趋向于负无穷舍入,包括半值
    • halfExpand: 默认值,半值远离 0 舍入
    • halfTrunc: 向 0 取整,包括半值
    • halfEven: 半值向最接近的偶数整数舍入,常用于统计,减少片差
  • trailingZeroDisplay
    • 整数末尾 0 的显示策略,可选的值包括:
    • auto: 默认值,根据 minimumFractionDigits 和 minimumSignificanntDigits 保持末尾 0
    • stripIfInteger: 如果小数部分全为 0 则删除小数部分,如果小数部分有任何非零数则与 auto 相同

其他选项

  • notation
    • 数字的显示格式,可选的值包括:
    • standard: 默认值,纯数字格式
    • scientific: 返回格式化数字的数量级
    • engineering: 返回能被 3 整除的 10 的指数
    • compact: 表示指数的字符串,默认使用 short 形式
  • compactDisplay
    • 仅当 notation 为 compact 时使用,可选的值包括:
    • short: 默认值
    • long
  • useGrouping
    • 是否使用分组分隔符,像千位分隔符或者千/十万/千万分隔符,可选的值包括:
    • always: 即使 locale 偏好不同也展示分组分隔符
    • auto: 根据 locale 偏好显示分组分隔符,也取决于货币
    • min2: 当一组数字至少有 2 位数字时显示分组分隔符
    • true: 同 always
    • false: 不展示分组分隔符
    • 当 notation 为 compact 时默认值为 min2,否则默认值为 auto
    • 字符串 true 和 false 会被转化为默认值
  • signDisplay
    • 何时显示数字符号,可选的值包括:
    • auto: 默认值
    • always: 总是显示
    • exceptZero: 正数和负数显示符号,但 0 不显示
    • negative: 仅显示负数的符号,不包括负零
    • never: 从不展示

格式化

format()方法会基于区域和格式化选项进行数字格式化。支持数字、大数和字符串。

数字可能因为太大或太小而丢失精度

const numberFormat = new Intl.NumberFormat("en-US");
console.log(numberFormat.format(987654321987654321));
// 987,654,321,987,654,300

但是使用大数就不会有问题

const numberFormat = new Intl.NumberFormat("en-US");
console.log(numberFormat.format(987654321987654321n));
// 987,654,321,987,654,321

字符串也不会丢失精度

const numberFormat = new Intl.NumberFormat("en-US");
console.log(numberFormat.format("987654321987654321"));
// 987,654,321,987,654,321

使用指数表示

const numberFormat = new Intl.NumberFormat("en-US");
const bigNum = 987654321987654321n;
console.log(numberFormat.format(`${bigNum}E-6`));
// 987,654,321,987.654

格式化分割成多部分

formatToParts()将会返回一个对象数组,包含格式化后的每一部分,适合用来自定义字符串格式化。

const number = 3500;

const formatter = new Intl.NumberFormat("de-DE", {
  style: "currency",
  currency: "EUR",
});

console.log(formatter.format(number));
// "3.500,00 €"

console.log(formatter.formatToParts(number));
// [
//   { type: "integer", value: "3" },
//   { type: "group", value: "." },
//   { type: "integer", value: "500" },
//   { type: "decimal", value: "," },
//   { type: "fraction", value: "00" },
//   { type: "literal", value: " " },
//   { type: "currency", value: "€" },
// ];

格式化数字范围

formatRange()返回一个字符串表示数字范围格式化后的内容。

const nf = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0,
});

console.log(nf.formatRange(3, 5));
// "$3 – $5"

如果开始值和结束值四舍五入值相同或者完全相同时则会添加近似等于符号。

console.log(nf.formatRange(2.9, 3.1));
// "~$3"

格式化数字范围分割成多部分

formatRangeToParts()返回一个对象数组,包含格式化后的每一部分,适合用来自定义数字字符串的格式化范围。

const startRange = 3500;
const endRange = 9500;

const formatter = new Intl.NumberFormat("de-DE", {
  style: "currency",
  currency: "EUR",
});

console.log(formatter.formatRange(startRange, endRange));
// "3.500,00–9.500,00 €"

console.log(formatter.formatRangeToParts(startRange, endRange));
// [
//   { type: "integer", value: "3", source: "startRange" },
//   { type: "group", value: ".", source: "startRange" },
//   { type: "integer", value: "500", source: "startRange" },
//   { type: "decimal", value: ",", source: "startRange" },
//   { type: "fraction", value: "00", source: "startRange" },
//   { type: "literal", value: "–", source: "shared" },
//   { type: "integer", value: "9", source: "endRange" },
//   { type: "group", value: ".", source: "endRange" },
//   { type: "integer", value: "500", source: "endRange" },
//   { type: "decimal", value: ",", source: "endRange" },
//   { type: "fraction", value: "00", source: "endRange" },
//   { type: "literal", value: " ", source: "shared" },
//   { type: "currency", value: "€", source: "shared" },
// ]

获取配置项

const de = new Intl.NumberFormat("de-DE", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 2,
  roundingIncrement: 5,
  roundingMode: "halfCeil",
});

const usedOptions = de.resolvedOptions();
console.log(usedOptions.locale); // "de-DE"
console.log(usedOptions.numberingSystem); // "latn"
console.log(usedOptions.compactDisplay); // undefined ("notation" not set to "compact")
console.log(usedOptions.currency); // "USD"
console.log(usedOptions.currencyDisplay); // "symbol"
console.log(usedOptions.currencySign); // "standard"
console.log(usedOptions.minimumIntegerDigits); // 1
console.log(usedOptions.minimumFractionDigits); // 2
console.log(usedOptions.maximumFractionDigits); // 2
console.log(usedOptions.minimumSignificantDigits); // undefined (maximumFractionDigits is set)
console.log(usedOptions.maximumSignificantDigits); // undefined (maximumFractionDigits is set)
console.log(usedOptions.notation); // "standard"
console.log(usedOptions.roundingIncrement); // 5
console.log(usedOptions.roundingMode); // halfCeil
console.log(usedOptions.roundingPriority); // auto
console.log(usedOptions.signDisplay); // "auto"
console.log(usedOptions.style); // "currency"
console.log(usedOptions.trailingZeroDisplay); // auto
console.log(usedOptions.useGrouping); // auto

判断返回支持的 locale

在给定的 locales 数组中判断出 NumberFormat 支持的 locales。但是可能每个浏览器支持的不大一样。

const locales = ["ban", "id-u-co-pinyin", "de-ID"];
const options = { localeMatcher: "lookup" };

console.log(Intl.NumberFormat.supportedLocalesOf(locales, options));
// ["id-u-co-pinyin", "de-ID"]

总结

Intl.NumberFormat用于根据语言和地区格式化数字内容,像把数字格式化为货币、百分比或带单位的本地化字符串,精确控制数字的小数位数、有效数字和整数部分的最小位数,设置丰富的舍入模式像四舍五入、向零舍入或银行家舍入法这些场景下都十分适用。

那个把代码写得亲妈都不认的同事,最后被劝退了🤷‍♂️

作者 ErpanOmer
2025年12月22日 12:03

大家好😁。

上上周,我们在例会上送别了团队里的一位技术大牛,阿K。

说实话,阿K 的技术底子很强。他能手写 Webpack 插件,熟读 ECMA 规范,对 Chrome 的渲染管线了如指掌。

但最终,CTO 还是决定劝退他了。

Suggestion.gif

理由很残酷,只有一句话: 你的代码,团队里没人敢接手。🤷‍♂️

为了所谓的极致性能,牺牲代码的可读性,到底值不值?


事件的开始

我们有一个很普通的后台管理系统重构。

阿K 负责最核心的权限校验模块。这本是一个很简单的逻辑:后端返回一个权限列表,前端判断一下用户有没有某个按钮的权限。

普通人(比如我)大概会这么写:

// 一眼就能看懂
const hasPermission = (userPermissions, requiredPermission) => {
  return userPermissions.includes(requiredPermission);
};

if (hasPermission(currentUser.permissions, 'DELETE_USER')) {
  showDeleteButton();
}

但是,阿K 看到这段代码时,露出了鄙夷的神情😒。

includes 这种遍历操作太慢了!我们要处理的是十万级的用户并发(并没有),必须优化!

于是,他闭关三天,重写了整个模块。

Code Review 的时候,我们所有人都傻了。屏幕上出现了一堆我们看不懂的天书😖:

// 全程位运算,没有任何注释
const P = { r: 1, w: 2, e: 4, d: 8 }; 
const _c = (u, p) => (u & p) === p;

// 这里甚至用了一个位移掩码生成的哈希表
const _m = (l) => l.reduce((a, c) => a | (P[c] || 0), 0);

// 像不像一段乱码?
const chk = (u, r) => _c(_m(u.r), P[r]);

我问他:阿K,这 _c_m 是啥意思?能加个注释吗?

阿K 振振有词: 好的代码不需要注释!位运算是计算机执行最快的操作,比字符串比对快几百倍!这不仅仅是代码,这是对 CPU 的尊重,是艺术!

我: 。 。 。 。🤣

在那个没有性能瓶颈的后台管理系统里,他为了那肉眼不可见的 0.0001 毫秒提升,制造了一个维护麻烦。


屎山💩崩溃的那一天

灾难发生在两个月后。

业务方突然提了一个需求: 权限逻辑要改,现在支持‘反向排除’权限,而且权限字段要从数字改成字符串组。

那天,阿K 正好去年假了,手机关机😒。

任务落到了刚入职的实习生小李头上。

小李打开 permission.js,看着满屏的 >>&| 和单字母变量,整个人僵在了工位上。

他试图去理解那个位移掩码的逻辑,但他发现,只要改动一个字符,整个系统的权限就全乱套了——管理员突然看不了页面,实习生突然能删库了🤔。

这代码有毒吧…… 小李在第 10 次尝试修复失败后,差点哭出来😭。

因为这个模块的逻辑过于晦涩,且和其他模块高度耦合(阿K 为了复用,把这些位运算逻辑注入到了全局原型链里),我们根本不敢动。

结果是:那个简单的需求,被硬生生拖了一周。 业务方投诉到了 CTO 那里。

CTO 看了眼代码,沉默了三分钟,然后问了一句:

写这玩意儿的人,是觉得以后都不用维护了吗?😥


过早优化是万恶之源 !

阿K 回来后,很不服气。他觉得是我们技术太菜,看不懂他的高级操作。

他拿出了 Chrome Profiler 的截图,指着那微乎其微的差距说:看!我的写法比你们快了 40%!

但他忽略了软件工程中最重要的一条公式:

代码价值 = (实现功能 + 可维护性) / 复杂度

过早优化是万恶之源 ! ! !

在 99% 的业务场景下,V8 引擎已经足够快了。

  • 你把 forEach 改成 while 倒序循环,性能确实提升了,但代码变得难读了。
  • 你把清晰的 switch-case 改成了晦涩的 lookup table 还没有类型提示,Bug 率上升了。
  • 你为了省几个字节的内存,用各种黑魔法操作对象,导致后来的人根本不敢碰😖。

这种所谓的性能优化,其实是程序员的自嗨。

它是用团队的维护成本,去换取机器那一瞬间的快感。它不是优化,它是给项目埋雷。


什么样的代码才是好代码?

后来,我们将阿K 的那坨代码 通过 chatGPT 全部推倒重写。

1️⃣ 权限定义(语义清晰)

// permissions.ts
export enum Permission {
  READ = 'read',
  WRITE = 'write',
  EDIT = 'edit',
  DELETE = 'delete',
}

2️⃣ 用户模型

// user.ts
import { Permission } from './permissions';

export interface User {
  id: string;
  permissions: Permission[];
}

3️⃣ 权限校验函数(核心)

// auth.ts
import { Permission } from './permissions';
import { User } from './user';

export function hasPermission(
  user: User,
  required: Permission
): boolean {
  return user.permissions.includes(required);
}

4️⃣ 批量权限校验

export function hasAllPermissions(
  user: User,
  required: Permission[]
): boolean {
  return required.every(p => user.permissions.includes(p));
}

export function hasAnyPermission(
  user: User,
  required: Permission[]
): boolean {
  return required.some(p => user.permissions.includes(p));
}

5️⃣ 判断方法

if (!hasPermission(user, Permission.DELETE)) {
  throw new Error('No permission to delete');
}

用回了用户权限结构清晰可见的,权限判断,一眼就懂。甚至都不需要注释🤷‍♂️

虽然跑分慢了那么一丁点(用户根本无感知),但任何一个新来的同事,只要 5 分钟就能看懂并上手修改。

这件事给我留下了深刻的教训:

  1. 代码是写给人看的,顺便给机器运行。

    如果一段代码只有你现在能看懂,那它就是垃圾代码;如果一段代码连你一个月后都看不懂,那它就是有害代码。

  2. 不要在非瓶颈处炫技。

    如果页面卡顿是因为 DOM 节点太多,你去优化 JS 的变量赋值速度,那就是隔靴搔痒。找到真正的瓶颈(Network, Layout, Paint),再对症下药。

  3. 可读性 > 巧技。

    简单的逻辑,是对同事最大的善意。


阿K 走的时候,还是觉得自己怀才不遇,觉得这家公司配不上他的技术🤣。

我祝他未来前程似锦。

但我更希望看到这篇文章的你,下次在想要按下键盘写一段绝妙的、只有你看懂的单行代码时,能停下来想一想:

如果明天我离职了,接手的人会不会骂娘?

你是脑残么.gif

毕竟,我们不想让亲妈都不认识代码,我们更不想让同事在那骂娘。

谢谢大家👏

Node.js 原生功能狂飙,15 个热门 npm 包要失业了

作者 Immerse
2025年12月22日 12:00

大家好,我是 Immerse,一名独立开发者、内容创作者、AGI 实践者。

关注公众号:沉浸式趣谈,获取最新文章(更多内容只在公众号更新)

个人网站:yaolifeng.com 也同步更新。

转载请在文章开头注明出处和版权信息。

我会在这里分享关于编程独立开发AI干货开源个人思考等内容。

如果本文对您有所帮助,欢迎动动小手指一键三连(点赞评论转发),给我一些支持和鼓励,谢谢!


之前装个 Node.js 项目,npm 包能装一大堆。

现在发现很多包其实不用装了,Node.js 自己就支持。

这次整理了 15 个已经被 Node.js 原生功能替代的热门 npm 包。

有些已经稳定了,有些还在实验阶段,但都能用起来了。

fetch 终于成全局函数了

以前在 Node.js 里用 fetch,必须装 node-fetch。

现在 Node.js 18 开始,fetch 已经是全局函数了,和浏览器里的用法完全一样。

const res = await fetch("https://api.github.com/repos/nodejs/node");
const data = await res.json();
console.log(data.full_name);

直接就能用,不用装任何包。

Node.js 17.5 开始实验性支持,到 18 就稳定了。

如果你的项目还在用 Node.js 18 之前的版本,那还是得装 node-fetch。

WebSocket 也原生支持了

之前做 WebSocket 客户端,基本都用 ws 这个包。

现在 Node.js 有了全局的 WebSocket 类。

const ws = new WebSocket("wss://echo.websocket.org");

ws.onopen = () => ws.send("Hello!");
ws.onmessage = (event) => console.log("Received:", event.data);

Node.js 21 加的,不过还是实验性的。

要注意的是,这只是客户端支持。

如果要做 WebSocket 服务端,还是得用 ws 或者其他库。

测试框架不用装了

以前写测试,要装 mocha、jest 这些框架。

现在 Node.js 自带测试模块 node:test。

import test from "node:test";
import assert from "node:assert";

test("addition works", () => {
  assert.strictEqual(2 + 2, 4);
});

Node.js 18 加的实验性功能,到 20 就稳定了。

如果需要快照测试、mock 这些高级功能,第三方框架还是更强。

不过对于模块级别的测试,node:test 完全够用了。

SQLite 也要原生支持了

之前用 SQLite,要装 sqlite3 或 better-sqlite3。

这俩包都需要编译原生模块,升级 Node.js 版本经常出问题。

现在 Node.js 在开发 node:sqlite 模块。

import { open } from "node:sqlite";

const db = await open(":memory:");
await db.exec("CREATE TABLE users (id INTEGER, name TEXT)");

不过还是实验性的,等稳定了就能彻底告别编译问题了。

控制台彩色输出不用装 chalk 了

给控制台输出加颜色,以前都用 chalk 或 kleur。

现在 Node.js 有 util.styleText 函数。

import { styleText } from "node:util";

console.log(styleText("red", "Error!"));
console.log(styleText(["bold", "green"], "Success!"));

Node.js 20.12 加的,到 22.17 就稳定了。

如果需要复杂的主题配置或链式调用,chalk 还是更好用。

但简单的颜色输出,原生的就够了。

清理 ANSI 码也不用装包了

以前要去掉日志里的 ANSI 转义码,得装 strip-ansi。

现在有 util.stripVTControlCharacters 函数。

import { stripVTControlCharacters } from "node:util";

const text = "\u001B[4mUnderlined\u001B[0m";
console.log(stripVTControlCharacters(text));

原生处理,稳定可靠。

基本不需要再装第三方包了。

glob 匹配文件也原生了

匹配文件路径,以前必须用 glob 包。

Node.js 22 开始有 fs.glob 函数了。

import fs from "node:fs/promises";

const files = await fs.glob("**/*.js");
console.log(files);

22 版本就稳定了,可以放心用。

老项目还在用旧版本 Node.js 的话,还是得继续用 glob 包。

递归删除目录不用 rimraf 了

删除整个目录树,以前都用 rimraf。

现在 fs.rm 直接支持递归删除。

import fs from "node:fs/promises";

await fs.rm("dist", { recursive: true, force: true });

Node.js 12.10 就有了,现在所有 LTS 版本都稳定支持。

递归创建目录也不用 mkdir 了

创建多级目录,以前要装 mkdir。

现在 fs.mkdir 原生支持。

await fs.mkdir("logs/app", { recursive: true });

Node.js 10.12 就加了,早就稳定了。

UUID 生成不用装包了

生成 UUID v4,以前要装 uuid 包。

现在 crypto 模块自带 randomUUID 函数。

import { randomUUID } from "node:crypto";

console.log(randomUUID());

Node.js 14.17 就有了,稳定版本。

Base64 编解码也原生支持了

以前要 polyfill atob 和 btoa 函数。

现在这俩已经是全局函数了。

const encoded = btoa("hello");
console.log(encoded);
console.log(atob(encoded));

Buffer 一直都有,现在加上 atob 和 btoa,浏览器和 Node.js 的代码终于统一了。

Node.js 20 左右加的,现在 LTS 版本都有。

URL 路由匹配有了 URLPattern

做路由匹配,以前要装 url-pattern。

现在有全局的 URLPattern API。

const pattern = new URLPattern({ pathname: "/users/:id" });
const match = pattern.exec("/users/42");
console.log(match.pathname.groups.id);

Node.js 20 加的,不过还是实验性的。

但已经能用了,而且和浏览器的 URLPattern 完全一样。

加载 .env 文件不一定要 dotenv 了

之前加载环境变量文件,必须装 dotenv。

现在可以用 --env-file 参数。

node --env-file=.env app.js

Node.js 20.10 加的实验性功能。

如果需要变量展开或多文件支持,dotenv 还是更强。

但简单场景下,原生的就够了。

EventTarget 也是全局的了

以前 Node.js 只有 EventEmitter,要用 Web 标准的 EventTarget 得装 event-target-shim。

现在 EventTarget 已经是全局的了。

const target = new EventTarget();
target.addEventListener("ping", () => console.log("pong"));
target.dispatchEvent(new Event("ping"));

Node.js 15 加的,15.4 就稳定了。

浏览器和 Node.js 终于可以用同样的事件 API 了。

运行 TypeScript 不一定要 tsc 了

以前运行 .ts 文件,要装 TypeScript 编译器或 ts-node。

现在 Node.js 有实验性的 TypeScript 支持。

node --experimental-strip-types app.ts

Node.js 21 加的实验性功能。

不过这只是去掉类型标注,不做类型检查。

生产环境还是得用完整的 TypeScript 工具链。

为啥 Node.js 要把这些功能内置

看这些变化,能发现一个趋势。

以前需要外部依赖的功能,现在越来越多变成了核心功能。

这样做有几个好处。

减少依赖数量,项目更轻量。

降低供应链攻击风险,不用担心某个包被投毒。

代码在浏览器和服务端之间更容易移植。

能用就用起来

这些原生功能,浏览器支持好的就可以直接用了。

实验性的功能可以在开发环境先试试。

【vue3】 + 【vite】 + 【rollup-plugin-obfuscator】混淆打包 => 打包报错

2025年12月22日 11:33

rollup-plugin-obfuscator 可以在基于 Vite 的 Vue 3 项目中使用,因为 Vite 本身就是基于 Rollup 构建的

npm install --save-dev rollup-plugin-obfuscator javascript-obfuscator

yarn add javascript-obfuscator -D


import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import obfuscator from 'rollup-plugin-obfuscator';
export default defineConfig({
  // base: "",
  build: {
    minify: 'esbuild', // 默认
  },
  esbuild: {
    drop: ['console', 'debugger'],//打包去除
  },
  plugins: [
    vue(),
    obfuscator({
      global:false,
      // options配置项实际为 javascript-obfuscator 选项,具体可查看https://github.com/javascript-obfuscator/javascript-obfuscator
      options: {
        compact: true,
        controlFlowFlattening: true,
        controlFlowFlatteningThreshold: 0.75,
        numbersToExpressions: true,
        simplify: true,
        stringArrayShuffle: true,
        splitStrings: true,
        splitStringsChunkLength: 10,
        rotateUnicodeArray: true,
        deadCodeInjection: true,
        deadCodeInjectionThreshold: 0.4,
        debugProtection: false,
        debugProtectionInterval: 2000,
        disableConsoleOutput: true,
        domainLock: [],
        identifierNamesGenerator: "hexadecimal",
        identifiersPrefix: "",
        inputFileName: "",
        log: true,
        renameGlobals: true,
        reservedNames: [],
        reservedStrings: [],
        seed: 0,
        selfDefending: true,
        sourceMap: false,
        sourceMapBaseUrl: "",
        sourceMapFileName: "",
        sourceMapMode: "separate",
        stringArray: true,
        stringArrayEncoding: ["base64"],
        stringArrayThreshold: 0.75,
        target: "browser",
        transformObjectKeys: true,
        unicodeEscapeSequence: true,

        domainLockRedirectUrl: "about:blank",
        forceTransformStrings: [],
        identifierNamesCache: null,
        identifiersDictionary: [],
        ignoreImports: true,
        optionsPreset: "default",
        renameProperties: false,
        renamePropertiesMode: "safe",
        sourceMapSourcesMode: "sources-content",
       
        stringArrayCallsTransform: true,
        stringArrayCallsTransformThreshold: 0.5,
       
        stringArrayIndexesType: ["hexadecimal-number"],
        stringArrayIndexShift: true,
        stringArrayRotate: true,
        stringArrayWrappersCount: 1,
        stringArrayWrappersChainedCalls: true,
        stringArrayWrappersParametersMaxCount: 2,
        stringArrayWrappersType: "variable",
      }
    })
  ]
})

打包报错……

⏰前端周刊第445期(2025年12月15日–12月21日)

2025年12月22日 11:19

📢 宣言每周更新国外论坛的前端热门文章,推荐大家阅读/翻译,紧跟时事,掌握前端技术动态,也为写作或突破新领域提供灵感~

欢迎大家访问:github.com/TUARAN/fron… 顺手点个 ⭐ star 支持,是我们持续输出的续航电池🔋✨!

在线网址:fwdc.pages.dev/

前端周刊封面


💬 推荐语

本期内容涵盖 HTML 现状、React2Shell 安全事件复盘与防护、可访问性与性能指标实践、现代 CSS 新能力,以及框架与工具链的最新趋势。


🗂 本期精选目录

🧭 Web 开发

🛠 工具

⚡ 性能

🧪 Demo

🎨 CSS

💡 JavaScript

⚛️ React

🧾 前端周刊 · 上周内容「价值判断大表」

分类 标题 核心主题 影响面 趋势性 可行动性 综合优先级 编辑部判断
Web State of HTML 2025 HTML 原生能力演进 ⭐⭐⭐⭐☆ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐☆ S 前端回归平台能力,必读
Web React2Shell 百万美元挑战 前端安全 / RSC ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐☆ S+ 架构级安全信号
Web React2Shell 事件复盘 前端攻击面 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐☆ S+ 企业前端必看
Web <time> 元素语义讨论 HTML 语义 ⭐⭐☆☆☆ ⭐⭐⭐☆☆ ⭐⭐☆☆☆ B 思想有趣,实用性一般
Web Dynamic Datalist API + 原生组件 ⭐⭐☆☆☆ ⭐⭐⭐☆☆ ⭐⭐⭐☆☆ B 可写 Demo
Web 衡量功能真实影响 产品 × 工程 ⭐⭐⭐⭐☆ ⭐⭐⭐☆☆ ⭐⭐⭐⭐☆ A 很适合写方法论
A11y WCAG 3.3.9 指南 可访问性 ⭐⭐⭐☆☆ ⭐⭐⭐⭐☆ ⭐⭐⭐☆☆ A- 企业项目价值高
工具 JS Bundler Grand Prix 构建工具对比 ⭐⭐⭐⭐☆ ⭐⭐⭐⭐☆ ⭐⭐⭐⭐☆ A 选型 & 分享利器
工具 Vitest 4 迁移指南 测试体系 ⭐⭐⭐☆☆ ⭐⭐⭐⭐☆ ⭐⭐⭐⭐☆ A 团队升级必备
性能 LCP 深度拆解 Core Web Vitals ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐☆ ⭐⭐⭐⭐☆ S 性能工程核心
性能 无限滚动 CLS 优化 页面稳定性 ⭐⭐⭐⭐☆ ⭐⭐⭐⭐☆ ⭐⭐⭐⭐☆ S- 实战价值极高
性能 屏幕尺寸懒加载 图片性能 ⭐⭐⭐☆☆ ⭐⭐⭐☆☆ ⭐⭐⭐☆☆ B 技巧型
Demo GSAP 曲线路径动画 动效 ⭐⭐☆☆☆ ⭐⭐⭐☆☆ ⭐⭐⭐☆☆ C+ 灵感向
Demo 页面过渡策略 UX ⭐⭐⭐☆☆ ⭐⭐⭐☆☆ ⭐⭐⭐☆☆ B 可做设计分享
Demo Toon Text CSS 创意 ⭐⭐☆☆☆ ⭐⭐⭐☆☆ ⭐⭐☆☆☆ C 收藏即可
CSS scroll-state(scrolled) 新 CSS API ⭐⭐⭐⭐☆ ⭐⭐⭐⭐☆ ⭐⭐⭐☆☆ A 值得跟踪
CSS Anchor Positioning 布局能力 ⭐⭐⭐⭐☆ ⭐⭐⭐⭐☆ ⭐⭐⭐☆☆ A 未来组件基础
CSS Masonry → grid-lanes 布局演进 ⭐⭐⭐⭐☆ ⭐⭐⭐⭐⭐ ⭐⭐⭐☆☆ A+ CSS 重大进展
CSS Dialog View Transitions 原生过渡 ⭐⭐⭐☆☆ ⭐⭐⭐⭐☆ ⭐⭐⭐☆☆ A- 框架替代信号
CSS VoxCSS CSS 引擎 ⭐⭐☆☆☆ ⭐⭐⭐☆☆ ⭐⭐☆☆☆ C 实验性
JS JS 内存效率 性能工程 ⭐⭐⭐⭐☆ ⭐⭐⭐☆☆ ⭐⭐⭐⭐☆ A 工程向干货
JS WebAssembly 使用时机 WASM ⭐⭐⭐☆☆ ⭐⭐⭐☆☆ ⭐⭐⭐☆☆ B 知道即可
JS 多品牌 Token 系统(Vue) 设计系统 ⭐⭐⭐⭐☆ ⭐⭐⭐⭐☆ ⭐⭐⭐⭐☆ A 企业前端价值高
JS 三大框架性能对比(2026) 框架选型 ⭐⭐⭐☆☆ ⭐⭐⭐☆☆ ⭐⭐☆☆☆ B- 参考,不迷信
React RSC Explorer RSC 可视化 ⭐⭐⭐⭐☆ ⭐⭐⭐⭐⭐ ⭐⭐⭐☆☆ A+ 理解 RSC 必备
React Next.js 国际化 工程实践 ⭐⭐⭐☆☆ ⭐⭐⭐☆☆ ⭐⭐⭐⭐☆ A- 实用指南
React Vite vs Webpack 构建体系 ⭐⭐⭐⭐☆ ⭐⭐⭐⭐☆ ⭐⭐⭐⭐☆ A 架构选型好文

Lexical 富文本编辑器组件详解

2025年12月22日 11:18

📚 Lexical 简介

Lexical 是 Meta 开源的基于 React 的富文本编辑器框架,用于替代 Draft.js,具有更好的性能、扩展性和稳定性。

🎯 核心特性

  • ✅ 高性能、可扩展
  • ✅ 无依赖、轻量级
  • ✅ 完整的 TypeScript 支持
  • ✅ 插件化架构
  • ✅ 嵌套编辑器支持

🔧 基础组件

1. LexicalComposer - 编辑器容器

jsx

import { LexicalComposer } from '@lexical/react/LexicalComposer';

function App() {
  const initialConfig = {
    namespace: 'MyEditor',
    theme: theme,
    nodes: [],
    onError: (error) => console.error(error),
  };

  return (
    <LexicalComposer initialConfig={initialConfig}>
      {/* 其他插件组件 */}
    </LexicalComposer>
  );
}

2. RichTextPlugin - 富文本核心

jsx

import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';

function Editor() {
  return (
    <>
      <RichTextPlugin
        contentEditable={
          <ContentEditable
            className="editor-input"
            style={{
              minHeight: '150px',
              border: '1px solid #ccc',
              padding: '10px',
            }}
          />
        }
        placeholder={
          <div className="editor-placeholder">输入内容...</div>
        }
        ErrorBoundary={LexicalErrorBoundary}
      />
      <HistoryPlugin />
    </>
  );
}

3. 完整的基础编辑器

jsx

import React from 'react';
import { 
  LexicalComposer, 
  RichTextPlugin,
  ContentEditable,
  HistoryPlugin,
  AutoFocusPlugin
} from '@lexical/react';
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
import { TableCellNode, TableNode, TableRowNode } from '@lexical/table';
import { ListNode, ListItemNode } from '@lexical/list';
import { CodeHighlightNode, CodeNode } from '@lexical/code';
import { AutoLinkNode, LinkNode } from '@lexical/link';
import { TRANSFORMERS } from '@lexical/markdown';
import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin';

const theme = {
  // 自定义样式
  text: {
    bold: 'editor-text-bold',
    italic: 'editor-text-italic',
    underline: 'editor-text-underline',
    strikethrough: 'editor-text-strikethrough',
  }
};

const initialConfig = {
  namespace: 'MyEditor',
  theme,
  nodes: [
    HeadingNode,
    QuoteNode,
    TableNode,
    TableCellNode,
    TableRowNode,
    ListNode,
    ListItemNode,
    CodeNode,
    CodeHighlightNode,
    AutoLinkNode,
    LinkNode
  ],
  onError: (error) => console.error(error),
};

function MyEditor() {
  return (
    <LexicalComposer initialConfig={initialConfig}>
      <div className="editor-container">
        <RichTextPlugin
          contentEditable={
            <ContentEditable className="editor-input" />
          }
          placeholder={
            <div className="editor-placeholder">开始写作...</div>
          }
        />
        <HistoryPlugin />
        <AutoFocusPlugin />
        <MarkdownShortcutPlugin transformers={TRANSFORMERS} />
      </div>
    </LexicalComposer>
  );
}

🔌 常用插件组件

1. ToolbarPlugin - 工具栏组件

jsx

import { 
  $getSelection, 
  $isRangeSelection, 
  FORMAT_TEXT_COMMAND 
} from 'lexical';

function ToolbarPlugin() {
  const [isBold, setIsBold] = useState(false);
  const [isItalic, setIsItalic] = useState(false);
  const editor = useLexicalComposerContext();

  const updateToolbar = useCallback(() => {
    const selection = $getSelection();
    if ($isRangeSelection(selection)) {
      setIsBold(selection.hasFormat('bold'));
      setIsItalic(selection.hasFormat('italic'));
    }
  }, []);

  // 监听编辑器状态变化
  useEffect(() => {
    return editor.registerUpdateListener(({ editorState }) => {
      editorState.read(() => {
        updateToolbar();
      });
    });
  }, [editor, updateToolbar]);

  return (
    <div className="toolbar">
      <button
        className={isBold ? 'active' : ''}
        onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')}
      >
        加粗
      </button>
      <button
        className={isItalic ? 'active' : ''}
        onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')}
      >
        斜体
      </button>
      <button
        onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')}
      >
        下划线
      </button>
    </div>
  );
}

2. ImagePlugin - 图片上传

jsx

import { INSERT_IMAGE_COMMAND } from './ImagePlugin';

function ImagePlugin() {
  const [editor] = useLexicalComposerContext();

  const handleImageUpload = useCallback((files: FileList) => {
    const reader = new FileReader();
    reader.onload = function () {
      if (typeof reader.result === 'string') {
        editor.dispatchCommand(INSERT_IMAGE_COMMAND, {
          altText: '图片',
          src: reader.result,
        });
      }
    };
    reader.readAsDataURL(files[0]);
  }, [editor]);

  return (
    <input
      type="file"
      accept="image/*"
      onChange={(e) => handleImageUpload(e.target.files)}
    />
  );
}

3. LinkPlugin - 链接处理

jsx

import { LinkPlugin as LexicalLinkPlugin } from '@lexical/react/LexicalLinkPlugin';
import { LinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';

function LinkPlugin() {
  const [editor] = useLexicalComposerContext();
  const [isLink, setIsLink] = useState(false);

  const insertLink = useCallback(() => {
    if (!isLink) {
      const url = prompt('输入链接地址:', 'https://');
      if (url) {
        editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
      }
    } else {
      editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
    }
  }, [editor, isLink]);

  return (
    <>
      <button onClick={insertLink}>
        {isLink ? '取消链接' : '添加链接'}
      </button>
      <LexicalLinkPlugin />
    </>
  );
}

📋 自定义节点组件

1. 自定义节点定义

jsx

// CustomNode.js
import { DecoratorNode } from 'lexical';

export class CustomNode extends DecoratorNode {
  static getType() {
    return 'custom';
  }

  static clone(node) {
    return new CustomNode(node.__key);
  }

  createDOM() {
    const div = document.createElement('div');
    div.className = 'custom-node';
    return div;
  }

  updateDOM() {
    return false;
  }

  decorate() {
    return <CustomComponent nodeKey={this.__key} />;
  }
}

// 自定义组件
function CustomComponent({ nodeKey }) {
  const [value, setValue] = useState('自定义内容');
  
  return (
    <div className="custom-component">
      <input
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
    </div>
  );
}

// 在配置中注册
const initialConfig = {
  nodes: [CustomNode, ...其他节点],
};

2. 自定义插件示例

jsx

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';


// 自定义键盘快捷键插件
function KeyboardShortcutPlugin() {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    const handleKeyDown = (event) => {
      // Ctrl + S 保存
      if ((event.ctrlKey || event.metaKey) && event.key === 's') {
        event.preventDefault();
        // 保存逻辑
        editor.update(() => {
          const editorState = editor.getEditorState();
          console.log('保存内容:', JSON.stringify(editorState.toJSON()));
        });
      }
      
      // Ctrl + B 加粗
      if ((event.ctrlKey || event.metaKey) && event.key === 'b') {
        event.preventDefault();
        editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
      }
    };

    return editor.registerRootListener((rootElement) => {
      if (rootElement) {
        rootElement.addEventListener('keydown', handleKeyDown);
        return () => {
          rootElement.removeEventListener('keydown', handleKeyDown);
        };
      }
    });
  }, [editor]);

  return null;
}

实际使用

import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';


import { VariableTextNode } from './node';
import styles from './style.module.css';

  const initialConfig = {
    namespace: 'prompt-composer',
    nodes: [VariableTextNode],
    onError: (error: Error) => {
      console.error(error);
    },
    theme: {
      paragraph: styles.editorParagraph,
    },
  };



<div className={styles.editorWrapper}>
  <LexicalComposer initialConfig={initialConfig}>
    <div
      className={cx(styles.editor, readOnly && styles.editorReadonly)}
      style={{ height, minHeight }}
    >
      <RichTextPlugin
        contentEditable={<ContentEditable className={styles.contentEditable} />}
        placeholder={<div className={styles.placeholder}>{placeholder || ''}</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
      <OnChangePlugin onChange={handleEditorChange} />
      <HistoryPlugin />
      <VariablePlugin variables={variables} />
      <VariableTransformerPlugin />
      <EditorUpdatePlugin value={value} />
      <OnBlurPlugin onBlur={onBlur} />
    </div>
  </LexicalComposer>
  {!readOnly && (
    <div className={styles.resizeHandle} onMouseDown={beginResize}>
      <div className={styles.resizeIcon} />
    </div>
  )}
</div>
//样式文件
.editorParagraph {
  margin: 0;
  position: relative;
}
//自定义节点
import { TextNode, SerializedTextNode, Spread, NodeKey, $applyNodeReplacement } from 'lexical';

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

export type SerializedVariableTextNode = Spread<
  {
    variableName: string;
  },
  SerializedTextNode
>;

// Custom Text Node to handle variable highlighting
export class VariableTextNode extends TextNode {
  __variableName: string;

  constructor(text: string, variableName: string, key?: NodeKey) {
    super(text, key);
    this.__variableName = variableName;
  }

  static getType(): string {
    return 'variable-text';
  }

  isSimpleText(): boolean {
    return false;
  }

  // Make this node behave as a single unit - cannot be partially selected or edited
  isTextEntity(): boolean {
    return true;
  }

  static clone(node: VariableTextNode): VariableTextNode {
    return new VariableTextNode(node.getTextContent(), node.__variableName, node.getKey());
  }

  createDOM(config: any): HTMLElement {
    const element = super.createDOM(config);
    element.className = styles.variableToken;
    element.dataset.variable = this.__variableName;
    return element;
  }

  updateDOM(prevNode: VariableTextNode, dom: HTMLElement, config: any): boolean {
    const isUpdated = super.updateDOM(prevNode as any, dom, config);
    if (prevNode.__variableName !== this.__variableName) {
      dom.dataset.variable = this.__variableName;
      return true;
    }
    return isUpdated;
  }

  exportJSON(): SerializedVariableTextNode {
    return {
      ...super.exportJSON(),
      variableName: this.__variableName,
      type: 'variable-text',
    };
  }

  static importJSON(serializedNode: SerializedVariableTextNode): VariableTextNode {
    const node = new VariableTextNode(serializedNode.text, serializedNode.variableName);
    node.setFormat(serializedNode.format);
    node.setDetail(serializedNode.detail);
    node.setMode(serializedNode.mode);
    node.setStyle(serializedNode.style);
    return node;
  }
}

export function $createVariableTextNode(text: string, variableName: string): VariableTextNode {
  return $applyNodeReplacement(new VariableTextNode(text, variableName));
}//创建并注册一个VariableTextNode节点实例



  • $applyNodeReplacement的作用

// 这是 Lexical 的内部工具函数 import { $applyNodeReplacement } from 'lexical';

// 它主要做三件事: function $applyNodeReplacement(newNode) { // 1. ✅ 检查节点是否已存在(通过 key) // 2. ✅ 如果存在,返回已存在的节点(避免重复) // 3. ✅ 如果不存在,注册新节点到编辑器状态 // 4. ✅ 确保节点在编辑器中被正确追踪 }

📝 总结

Lexical 提供了:

  1. 模块化架构 - 按需加载插件
  2. 完全控制 - 自定义节点和命令
  3. 高性能 - 基于不可变数据
  4. 扩展性强 - 支持复杂需求

推荐使用模式:

jsx

<LexicalComposer>
  <Toolbar />
  <RichTextPlugin />
  <EssentialPlugins />
  <CustomPlugins />
  <UtilityPlugins />
</LexicalComposer>
  • "@lexical/code": "^0.38.2"
  • "@lexical/link": "^0.38.2"
  • "@lexical/list": "^0.38.2"
  • "@lexical/react": "^0.16.0"
  • "@lexical/selection": "^0.38.2"
  • "@lexical/text": "^0.38.2"
  • "@lexical/utils": "^0.38.2"

【vue3】 + 【vite】 + 【vite-plugin-obfuscator】混淆打包 => 放弃了,样式会丢

2025年12月22日 11:15

vite-plugin-obfuscator 可以将你的代码进行混淆,一个依赖


安装

npm install vite-plugin-obfuscator --save-dev

配置文件引入和配置

import { viteObfuscateFile } from 'vite-plugin-obfuscator';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    viteMockServe({
      mockPath: 'mock',
      localEnabled: true
    }),
    viteObfuscateFile({
      options: {
        debugProtection: true
      }
    })
  ],

报错:

无法找到模块“vite-plugin-obfuscator”的声明文件。

没有具体步骤,这个依赖缺少类型声明,ts进行报错,给它一个声明就行,例如:

// 添加 vite-plugin-obfuscator 的类型声明
declare module 'vite-plugin-obfuscator' {
  import { Plugin } from 'vite';

  interface ViteObfuscateFileOptions {
    options?: any;
  }

  export function viteObfuscateFile(options?: ViteObfuscateFileOptions): Plugin;
}

具体的混淆配置:

compact boolean true 压缩代码,移除空格和换行符。 样式丢失
debugProtection boolean false 防止在开发者工具中调试代码。
----------------- --------- ------- --------------
renameGlobals boolean false 重命名全局变量和函数名。 接口路径失效
--------------- --------- ------- ------------ ---
renameProperties boolean false 重命名对象的属性名。 样式丢失?
transformObjectKeys boolean false 转换对象的键名,增加代码的复杂性。 样式丢失?

难搞啊,样式会丢

高德地图-物流路线

作者 星_离
2025年12月22日 11:13

有些时候我们的项目只使用原生一些内容是无法实现一些功能的,所以今天我带来了一个大家都熟悉的,也是生活中常见的一个功能,也就是大家在网购的时候,下单成功后就可以看到自己的订单,当然也可以查看物流信息,那么物流信息中有一个部分就是地图部分,这部分可以让用户看到自己购买的商品到了哪里。那这个功能我们使用原生大概率是无法完成的,这就需要我们使用高德地图、百度地图或者腾讯之类的开放地图类 API 的功能,那么今天我就来和大家分享一下如何去使用高德地图实现这一功能。

1. 准备工作

1.1. 官方文档

lbs.amap.com/api/javascr…

1.2. 需要安装的依赖

npm i @amap/amap-jsapi-loader --save

2. 开始

首先我们需要给地图设置一个容器,命名为container

<template>
  <div id="container"></div>
</template>

设置样式

<style  scoped>
  #container{
      padding:0px;
      margin: 0px;
      width: 100%;
      height: 800px;
  }
</style>

2.1. 创建地图组件

首先我们需要去扩展 window 接口类型的定义,如果不配置就会出现错误:

核心原因:

TypeScript 对 window 的类型有严格定义,默认的 Window 接口里没有 _AMapSecurityConfig,所以会提示 “该属性不存在”。但是高德地图又需要这个属性来配置安全密钥,所以我们就需要来扩展一下 window 类型。

那么我们就需要先来配置一下:按照以下路径创建 global.d.ts 文件

src-->types-->global.d.ts

进入文件配置以下内容:

interface Window {
  _AMapSecurityConfig: {
      securityJsCode: string
  }
}

2.2. 初始化地图组件

<script setup lang="ts">
import  {onMounted,onUnmounted} from "vue";
import AMapLoader from '@amap/amap-jsapi-loader';

let map = null;
onMounted(()=>{
  window._AMapSecurityConfig = {
    securityJsCode: "379c75538f6ae27ee95c983a6feaf358",
  };
  AMapLoader.load({
    key:"3d0735cef9dc47489452066b7dbe2510",
    version:"2.0",
    plugins:["AMap.scale"]
  })
    .then((AMap)=>{
      map = new AMap.Map("container",{
        //设置地图容器的Id
        viewMode:"3D",//是否为3D地图模式
        zoom:11,//初始化地图级别
        center:[116.397428, 39.90923]
      })
    })
    .catch((e)=>{
      console.error(e)
    })
})
onUnmounted(()=>{
  map?.destroy();
})
</script>

3. 路线规划

lbs.amap.com/demo/javasc…

通过数据处理出起始点和途径点的坐标:

const logisticsInfo = [
  {
    "latitude": "23.129152403638752",
    "longitude": "113.42775362698366"
  },
  {
    "latitude": "30.454012",
    "longitude": "114.42659"
  },
  {
    "latitude": "31.93182",
    "longitude": "118.633415"
  },
  {
    "latitude": "31.035032",
    "longitude": "121.611504"
  }
]
// 物流轨迹的起始点
  const start = logisticsInfo.shift()//起点
  const end = logisticsInfo.pop()//终点
  const ways = logisticsInfo.map(item => [item.longitude, item.latitude])//途径点数组
AMap.plugin('AMap.Driving', () => {
  //构造路线导航类
  var driving = new AMap.Driving({
    map: map, // 指定绘制的路线轨迹显示到map地图
    showTraffic: false, // 关闭实时交通路况
    hideMarkers: false // 隐藏默认的图标
  });
  // 根据起终点经纬度规划驾车导航路线
  driving.search(new AMap.LngLat(start.longitude, start.latitude), new AMap.LngLat(end.longitude, end.latitude), {
    waypoints:ways, // 途经点这里是一个二维数组的格式[[经度, 维度], [经度, 维度]]
  },function (status: string, result: object) {

    if (status === 'complete') {
      console.log('绘制驾车路线完成')
      // 调整视野达到最佳显示区域
      map.setFitView([ startMarker, endMarker, currentMarker ])
    } else {
      console.log('获取驾车数据失败:' + result)
    }
  })
})

4. 自定义图标

import startImg from '../public/start.png'
import endImg from '../public/end.png'
import carImg from '../public/car.png'

自定义图标需要使用到 marker 类

// 自定义开始坐标图片
const startMarker = new AMap.Marker({
  position: [start.longitude, start.latitude], // 自定义图标位置
  icon:startImg,
  map: map // 指定图标显示在哪个地图实例
})
// 自定义终点坐标图片
const endMarker = new AMap.Marker({
  position: [end.longitude, end.latitude],
  icon:endImg,
  map: map
})
// 自定义当前坐标图片
const currentMarker = new AMap.Marker({
  position: [currentLocationInfo.longitude, currentLocationInfo.latitude],
  icon:carImg,
  map: map
})

5. 完整代码实现

<template>
  <h1>地图组件</h1>
  <div id="container" style="width:100%; height: 500px;"></div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import AMapLoader from '@amap/amap-jsapi-loader'
import startImg from '../public/start.png'
import endImg from '../public/end.png'
import carImg from '../public/car.png'
// 接口返回的数据
const logisticsInfo = [
  {
    "latitude": "23.129152403638752",
    "longitude": "113.42775362698366"
  },
  {
    "latitude": "30.454012",
    "longitude": "114.42659"
  },
  {
    "latitude": "31.93182",
    "longitude": "118.633415"
  },
  {
    "latitude": "31.035032",
    "longitude": "121.611504"
  }
]
// 当前坐标
const currentLocationInfo = {
  latitude: "31.93182",
  longitude: "118.633415"
}
window._AMapSecurityConfig = {
  securityJsCode: '2af1e64a8f6b16d6d79bfa8162c46755'
}
onMounted(async () => {
  const AMap = await AMapLoader.load({
    key: '9ac7a2671565e21bc21aca6df07eb5cb',
    version: '2.0'
  })
  // 地图的创建
  var map = new AMap.Map('container', {
    viewMode: '2D', // 默认使用 2D 模式,如果希望使用带有俯仰角的 3D 模式,请设置 viewMode: '3D'
    zoom:16, // 初始化地图层级
    center: [116.209804,40.149393], // 初始化地图中心点
    plugins:["AMap.Driving"]
  });


  // 物流轨迹的起始点
  const start = logisticsInfo.shift()
  const end = logisticsInfo.pop()
  const ways = logisticsInfo.map(item => [item.longitude, item.latitude])
  // 自定义开始坐标图片
  const startMarker = new AMap.Marker({
    position: [start.longitude, start.latitude], // 自定义图标位置
    icon:startImg,
    map: map // 指定图标显示在哪个地图实例
  })
  // 自定义终点坐标图片
  const endMarker = new AMap.Marker({
    position: [end.longitude, end.latitude],
    icon:endImg,
    map: map
  })
// 自定义当前坐标图片
  const currentMarker = new AMap.Marker({
    position: [currentLocationInfo.longitude, currentLocationInfo.latitude],
    icon:carImg,
    map: map
  })

  // 绘制物流轨迹
  AMap.plugin('AMap.Driving', () => {
    //构造路线导航类
    var driving = new AMap.Driving({
      map: map, // 指定绘制的路线轨迹显示到map地图
      showTraffic: false, // 关闭实时交通路况
      hideMarkers: true // 隐藏默认的图标
    });
    // 根据起终点经纬度规划驾车导航路线
    driving.search(new AMap.LngLat(start.longitude, start.latitude), new AMap.LngLat(end.longitude, end.latitude), {
      waypoints:ways, // 途经点这里是一个二维数组的格式[[经度, 维度], [经度, 维度]]
    },function (status: string, result: object) {

      if (status === 'complete') {
        console.log('绘制驾车路线完成')
        // 调整视野达到最佳显示区域
        map.setFitView([ startMarker, endMarker, currentMarker ])
      } else {
        console.log('获取驾车数据失败:' + result)
      }
    })
  })

})
</script>

⏰前端周刊第444期(2025年12月8日–12月14日)

2025年12月22日 11:10

📢 宣言

每周更新国外论坛的前端热门文章,推荐大家阅读/翻译,紧跟时事,掌握前端技术动态,也为写作或突破新领域提供灵感~

欢迎大家访问:github.com/TUARAN/fron… 顺手点个 ⭐ star 支持,是我们持续输出的续航电池🔋✨!

在线网址:fwdc.pages.dev/

bannerv2.png


💬 推荐语

本期周刊聚焦前端技术的性能与创新:从 React Server Components 的安全性提升到 CSS 滚动触发动画的原生实现,从 TypeScript 7 原生编译器的影响到 Deno 工具链的持续演进。CSS Wrapped 2025 回顾了 CSS 的重大进展,性能优化深入到复杂 Web 应用的底层机制,前端开发正向着更安全、更高效、更原生的方向迈进。


🗂 本期精选目录

🧭 Web 开发

⚡ 性能

🛠 工具

🎨 CSS

💡 JavaScript

TypeScript

React

Angular


📌 小结

从 React Server Components 的安全性修复,到 CSS 原生滚动动画的突破;从 TypeScript 7 原生编译器的影响分析,到 ES2026 对 JavaScript 痛点的解决,这一周的前端技术展现出"性能优化 + 安全加固 + 原生能力增强"的多维度发展。CSS Wrapped 2025 总结了 CSS 在状态管理和逻辑处理方面的巨大进步,前端开发正在获得更强大的原生能力,同时也需要更加关注安全性和性能优化。


✅ OK,以上就是本次分享,欢迎加我们威 atar24,备注「前端周刊」,我们会邀请你进交流群👇

🚀 每周分享技术干货 🎁 不定期抽奖福利 💬 有问必答,群友互助

React 组件通讯全攻略:拒绝 "Props" 焦虑,掌握数据流动的艺术

2025年12月22日 11:06

前言

React 的开发旅程中,组件就像是乐高积木,我们把它们一个个搭起来构建出复杂的页面。但光搭起来还不够,这些积木之间必须有“电流”流通——这就是数据。
React 的设计哲学是单向数据流,就像瀑布一样,水流默认只能从高处流向低处。但在实际业务中,我们经常需要逆流而上,或者在两个平行的水池间交换水流。今天我们就来盘点 React 中最主流的四种“引水”方式,打通你的组件经脉。


1. 父传子:顺流而下的 Props

这是 React 中最自然、最基础的通信方式。想象一下,父亲给孩子零花钱,父亲(父组件)只需要要把钱(数据)递过去,孩子(子组件)伸手接着就行。
在代码层面,父组件通过在子组件标签上写上自定义属性(msg)来传递数据。

import Child from "./Child"
export default function Parent() {
  const state = {
    name: '小饶'
  }
  
  return (
    <div>
      <h2>父组件</h2>
      {/* 父亲把 '小饶' 这个名字打包进 msg 属性传给孩子 */}
      <Child msg={state.name} />
    </div>
  )
}

孩子组件这边,所有接收到的礼物都装在一个叫 props 的盒子里。不过要注意,Props 是只读的——这意味着孩子只能使用这些数据,不能直接修改它。就像孩子不能自己修改父亲银行卡的余额一样,如果要改,必须请求父亲操作。

export default function Child(props) {
  // 打开盒子看看收到了什么
  console.log(props);
  
  return (
    <h3>子组件 -- {props.msg}</h3>
  )
}

2. 子传父:给孩子一个“遥控器”

既然水流默认向下,那子组件想要改变父组件的数据该怎么办?比如,孩子想告诉父亲:“我考了 100 分,请更新一下你的心情状态”。
这时候,父组件需要提前准备一个“遥控器”——也就是一个函数。父组件把这个函数通过 props 传给子组件,当子组件需要通信时,就按下这个遥控器(调用函数),并把数据作为参数传回去。 **父组件: 准备好 getNum 函数,用来接收数据并更新自己的 count。 **

import Child from "./Child"
import { useState } from 'react'

export default function Parent() {
  let [count, setCount] = useState(1)  

  // 这就是那个“遥控器”函数
  const getNum = (n) => {
    setCount(n)
  }

  return (
    <div>
      <h2>父组件二 -- {count}</h2>
      {/* 把遥控器交给孩子 */}
      <Child getNum={getNum}></Child>
    </div>
  )
}

子组件: 在合适的时机(比如点击按钮时),通过 props 拿到并按下这个“遥控器”。

export default function Child(props) {
  
  const state = {
    num: 100
  }

  function send() {
    // 此时调用的是父组件里的函数,把 100 传了回去
    props.getNum(state.num)
  }

  return (
    <div>
      <h3>子组件二</h3>
      <button onClick={send}>发送</button>
    </div>
  )
}

3. 兄弟组件:找个共同的“家长”

兄弟组件之间没有直接的连线,就像你和你的表弟住在不同的屋子里,想聊天得通过大厅里的长辈传话。 这种模式在 React 中通常被称为状态提升。既然 Brother1 想要给 Brother2 传值,那我们就把这个值保存在他们共同的父亲身上。

  • Brother1 -> Parent:Brother1 先把数据传给父亲(利用上面的子传父技巧)。
  • Parent -> Brother2:父亲拿到数据后,更新自己的状态,再把这个新状态顺手传给 Brother2(利用父传子技巧)。

父组件(中间枢纽):

import { useState } from "react"
import Child1 from "./Child1"
import Child2 from "./Child2"

export default function Parent() {
  let [message, setMessage] = useState()

  // 接收老大传来的消息
  const getMsg = (msg) => {
    setMessage(msg)
  }

  return (
    <div>
      <h2> 父组件三 </h2>
      {/* 接收者:从 Child1 收信 */}
      <Child1 getMsg={getMsg} />
      {/* 发送者:把信转交给 Child2 */}
      <Child2 message={message} />
    </div>
  )
}

Child1(消息发送方):

export default function Child1(props) {
  const state = {
    msg: '1 中的数据'
  }

  function send() {
    props.getMsg(state.msg)
  }
  
  return (
    <div>
      <h3>子组件1</h3>
      <button onClick={send}>1</button>
    </div>
  )
}

Child2(消息接收方):

export default function Child2(props) {
  return (
    <div>
      {/* 坐等父亲把兄弟的消息送过来 */}
      <h3>子组件2 --- {props.message}</h3>
    </div>
  )
}

4. 跨代组件通信:Context 传送门

如果组件层级很深,比如“爷爷 -> 爸爸 -> 儿子 -> 孙子”,如果还用 Props 一层层传,那中间的爸爸和儿子就成了无辜的“搬运工”,代码会变得非常臃肿麻烦。
为了解决这个问题,React 提供了一个 Context(上下文)机制。这就像在家族里设立了一个“广播站”,爷爷在顶层广播,底下的任何一代子孙,只要想听,就可以直接接收信号,完全不需要中间人转手。
爷组件(数据源头):
我们需要先 createContext 创建一个信号塔,然后用 <Context.Provider> 把所有后代包起来,value 就是我们要广播的数据。

import Parent from "./Parent"
import { createContext } from 'react'

export const Context = createContext()  // 1. 建立信号塔

export default function Grand() {

  return (
    <div>
      <h2> 爷组件 </h2>
      {/* 2. 发射信号,内容是 value 中的数据 */}
      <Context.Provider value= {'爷组件的数据'}>
        <Parent/>
      </Context.Provider>
    </div>
  )
}

父组件(路人甲):
你看,父组件完全不需要碰这些数据,它只需要安静地渲染它的子组件即可。

import Child from "./Child"
export default function Parent() {

  return (
    <div>
      <h3>父组件</h3>
      <Child></Child>
    </div>
  )
}

孙子组件(数据接收者):
孙子组件不需要管它离爷爷隔了多少代,直接用 useContext 这个钩子函数,就能连上信号塔拿到数据。

import { useContext } from 'react'
import { Context } from './Grand'  // 3. 引入信号塔定义

export default function Child() {
  // 4. 接收信号
  const msg = useContext(Context)

  return (
    <div>
      <h4>孙子组件 --- {msg}</h4>
    </div>
  )
}

结语

组件通信是 React 开发中最基本也最重要的内功。

  • 简单的父子关系,PropsCallback 是最轻量的选择;
  • 兄弟组件,记得找共同的父级帮忙周转;
  • 当层级太深感到繁琐时,Context 就是你的救星。

掌握了这四招,你就能从容应对绝大多数的组件交互场景,让数据在你的应用中流动得井井有条。

利用 WebMKS 和 Java 实现前端访问虚拟机网页

2025年12月22日 10:55

实战干货:手把手教你用 WebMKS 和 Java 搞定 ESXi 虚拟机网页控制台

最近在折腾虚拟机网页控制台,发现用 VMware 原生的 WebMKS SDK 配合 ESXi 的 API 效果最好。这套方案不用装插件,直接在浏览器里就能操作。把中间踩过的坑和实现逻辑整理出来,希望能帮到大家。


1. 准备工作:依赖得放本地

WebMKS 这个 SDK 比较老派,它离不开 jQuery。为了稳妥,别用 CDN,直接把这几个文件下载下来放到你项目的 public/static/wmks 目录下。

怎么引入?

index.html 里按这个顺序排好,jQuery 必须在最前面,不然 SDK 报错。

HTML

<head>
  <link rel="stylesheet" href="/static/wmks/css/wmks-all.css">
  <script src="/static/wmks/jquery.min.js"></script>
  <script src="/static/wmks/jquery-ui.min.js"></script>
  <script src="/static/wmks/wmks.min.js"></script>
</head>

2. 后端:Java 怎么拿 Ticket

控制台连接前得先要个“门票”(Ticket)。后端用 Java 调用官方的 vijava 库,去 ESXi 那里申请一个。

核心逻辑:

  1. 连上 ESXi。
  2. 靠 UUID 找到那台虚拟机。
  3. 申请一个类型叫 webmks 的票据。

Java

public TicketInfo getVmTicket(String host, String user, String pwd, String vmUuid) throws Exception {
    // 建立连接
    ServiceInstance si = new ServiceInstance(new URL("https://" + host + "/sdk"), user, pwd, true);
    try {
        InventoryNavigator nav = new InventoryNavigator(si.getRootFolder());
        VirtualMachine vm = (VirtualMachine) nav.searchManagedEntity("VirtualMachine", vmUuid);
        
        // 关键:拿 webmks 票据
        VirtualMachineTicket vmt = vm.acquireTicket("webmks");
        
        // 把票据、主机IP、端口发给前端
        return new TicketInfo(vmt.getTicket(), vmt.getHost(), 443);
    } finally {
        si.getServerConnection().logout(); // 记得退出登录,不然会话堆积
    }
}

3. 前端:Vue 3 里的核心控制

初始化连接

在 Vue 里的思路是:拿到票据后,立刻初始化 SDK 并连上 WebSocket。

TypeScript

const startWMKS = (ticket: string) => {
  const WMKS_LIB = window.WMKS;
  
  // 初始化配置
  wmksInstance.value = WMKS_LIB.createWMKS("wmks-container", {
    enableHints: true,
    changeResolutionOnResizing: true, // 窗口变了,分辨率也跟着变
    rescaleOnParentResize: true,      // 自动缩放
    position: 'absolute'
  });

  // 连上 ESXi 的 443 端口
  wmksInstance.value.connect(`wss://${activeVM.value.host}:443/ticket/${ticket}`);
};

全屏怎么变清晰?

全屏时如果直接拉伸,画面会糊。我的办法是:先断开,变全屏,再重连。虽然多等了半秒,但画面会根据全屏后的尺寸重新协商,清晰度瞬间提升。


4. Windows 虚拟机的专项关照

搞 Linux 基本没啥事,但 Windows 挺挑剔,这几点没做到肯定出问题:

  1. 必装 VMware Tools:不装的话,显卡驱动不行,画面传输慢甚至直接黑屏。
  2. 显存给够:Windows 10 这种系统,显存起码给到 128MB
  3. CAD 按钮:Windows 登录得按 Ctrl+Alt+Del。网页里按没用,必须给用户做个按钮调用 wmksInstance.sendCAD()

5. 样式:别留白边

为了让黑色背景铺满整个弹窗,CSS 得这么写:

SCSS

/* 弹窗样式 */
:deep(.vnc-session-dialog) {
  display: flex;
  flex-direction: column;
  background: #000 !important;

  /* 全屏模式强行占满 */
  &.is-fullscreen { 
    height: 100vh !important;
    .el-dialog__body { flex: 1; display: flex; flex-direction: column; }
  }

  /* 核心:让 body 自动撑开 */
  .el-dialog__body {
    flex: 1;
    padding: 0 !important;
    overflow: hidden;
  }
}

总结一下

做这玩意儿逻辑其实不难,关键在于细节:

  • 顺序:jQuery 必须先引。
  • 时机:全屏重连能解决画面糊的问题。
  • 补丁:给 Windows 留个 CAD 按钮,装好 VMware Tools。

只要这几点对齐了,剩下的就是调 UI 让它看着更顺手的事儿了。 最后放上图片和源码

<template>
  <div class="vnc-app-wrapper">
    <div class="page-header">
      <div class="brand">
        <el-icon :size="28" color="#409eff"><Monitor /></el-icon>
        <h1>VMware 虚拟化资源管理</h1>
      </div>
    </div>

    <div class="vm-grid">
      <div v-for="(vm, index) in vmList" :key="index" class="vm-mini-card">
        <div class="vm-status-bar" :class="vm.status"></div>
        <div class="vm-card-body">
          <div class="vm-icon">
            <Platform v-if="vm.os === 'windows'" />
            <Cpu v-else />
          </div>
          <div class="vm-details">
            <h3 class="vm-name">{{ vm.name }}</h3>
            <p class="vm-ip">{{ vm.host }}</p>
          </div>
          <div class="vm-actions">
            <el-button type="primary" plain size="small" @click="openConsole(vm)">
              连接控制台
            </el-button>
          </div>
        </div>
        <div class="vm-footer">
          <span>UUID: {{ vm.vmUuid.substring(0, 14) }}...</span>
        </div>
      </div>
    </div>

    <el-dialog 
      v-model="vncDialogVisible" 
      :fullscreen="isFullscreen"
      :width="isFullscreen ? '100%' : '1100px'" 
      top="5vh"
      :draggable="true"
      :show-close="false"
      class="vnc-session-dialog"
      :before-close="handleCloseVNC"
    >
      <template #header>
        <div class="session-header drag-area">
          <div class="session-info">
            <div class="status-indicator" :class="connectionClass"></div>
            <span class="session-name">
              {{ activeVM?.name }} | {{ activeVM?.host }}
            </span>
          </div>
          <div class="session-controls">
            <el-button size="small" link @click="sendCAD">CAD</el-button>
            <div class="divider"></div>
            <el-button size="small" link @click="toggleFullscreen">
              <el-icon :size="16">
                <FullScreen v-if="!isFullscreen" />
                <CopyDocument v-else />
              </el-icon>
            </el-button>
            <el-button size="small" link type="danger" @click="handleCloseVNC">
              <el-icon :size="16"><Close /></el-icon>
            </el-button>
          </div>
        </div>
      </template>

      <div 
        class="session-stage" 
        v-loading="reloading" 
        element-loading-background="rgba(0, 0, 0, 0.9)"
        element-loading-text="正在重构画面..."
      >
        <div id="wmks-container"></div>
      </div>
    </el-dialog>
  </div>
</template>

<script setup lang="ts">
import { ref, nextTick, computed, onBeforeUnmount } from 'vue'
import { ElMessage } from 'element-plus'
import { Monitor, FullScreen, Close, CopyDocument, Platform, Cpu } from '@element-plus/icons-vue'
import { getVmConsoleTicket } from '@/api/taskManagement/vmConsole'


const vmList = ref([
  { 
    name: 'Windows-测试机', 
    host: '', 
    username: '', 
    password: '', 
    vmUuid: '',
    os: 'windows',
    status: 'online'
  },
  { 
    name: 'Linux-生产机', 
    host: '', 
    username: '', 
    password: '', 
    vmUuid: '',
    os: 'linux',
    status: 'online'
  }
])

const activeVM = ref<any>(null)
const vncDialogVisible = ref(false)
const isFullscreen = ref(false)
const reloading = ref(false)
const vncStatus = ref('disconnected')
const wmksInstance = ref<any>(null)

const connectionClass = computed(() => ({
  'is-connecting': vncStatus.value === 'connecting',
  'is-connected': vncStatus.value === 'connected'
}))


const openConsole = (vm: any) => {
  activeVM.value = vm;
  handleConnect(false);
}

const toggleFullscreen = async () => {
  reloading.value = true;
  if (wmksInstance.value) {
    wmksInstance.value.disconnect();
    wmksInstance.value.destroy();
    wmksInstance.value = null;
  }
  isFullscreen.value = !isFullscreen.value;
  await nextTick();
  setTimeout(async () => {
    try { await handleConnect(true); } finally { reloading.value = false; }
  }, 500);
}

const handleConnect = async (isRetry = false) => {
  const win = window as any;
  if (!win.WMKS) return ElMessage.error("SDK 未加载");

  try {
    vncStatus.value = 'connecting';
    if (!isRetry) vncDialogVisible.value = true;

    const res = await getVmConsoleTicket({
      host: activeVM.value.host,
      username: activeVM.value.username,
      password: activeVM.value.password,
      vmUuid: activeVM.value.vmUuid
    });
    
    const ticket = res.data?.ticket || res.ticket || res;
    await nextTick();
    startWMKS(ticket);
  } catch (err) {
    ElMessage.error("Ticket 申请失败,请检查机房连接");
    vncStatus.value = 'disconnected';
  }
}

const startWMKS = (ticket: string) => {
  const WMKS_LIB = (window as any).WMKS;
  wmksInstance.value = WMKS_LIB.createWMKS("wmks-container", {
    enableHints: true,
    useVNCHandshake: false,
    changeResolutionOnResizing: true,
    rescaleOnParentResize: true,
    position: 'absolute'
  });
  
  wmksInstance.value.register(WMKS_LIB.CONST.Events.CONNECTION_STATE_CHANGE, (evt: any, data: any) => {
    if (data.state === WMKS_LIB.CONST.ConnectionState.CONNECTED) {
      vncStatus.value = 'connected';
    }
  });

  wmksInstance.value.connect(`wss://${activeVM.value.host}:443/ticket/${ticket}`);
}

const sendCAD = () => wmksInstance.value?.sendCAD();
const handleCloseVNC = () => {
  if (wmksInstance.value) {
    wmksInstance.value.disconnect();
    wmksInstance.value.destroy();
    wmksInstance.value = null;
  }
  vncDialogVisible.value = false;
  isFullscreen.value = false;
  vncStatus.value = 'disconnected';
}

onBeforeUnmount(handleCloseVNC);
</script>

<style scoped lang="scss">
.vnc-app-wrapper {
  padding: 40px;
  background-color: #f8fafc;
  min-height: 100vh;
}

.page-header {
  max-width: 1200px;
  margin: 0 auto 40px;
  .brand {
    display: flex; align-items: center; gap: 12px;
    h1 { font-size: 22px; color: #334155; margin: 0; }
  }
}

.vm-grid {
  max-width: 1200px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 24px;
}

.vm-mini-card {
  background: #fff; border-radius: 12px; overflow: hidden;
  border: 1px solid #e2e8f0; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);
  transition: transform 0.2s;
  &:hover { transform: translateY(-4px); }

  .vm-status-bar { 
    height: 4px; background: #cbd5e1;
    &.online { background: #10b981; }
  }
  
  .vm-card-body {
    padding: 24px; display: flex; flex-direction: column; align-items: center;
    .vm-icon {
      font-size: 32px; color: #3b82f6; margin-bottom: 16px;
      padding: 12px; background: #eff6ff; border-radius: 12px;
    }
    .vm-name { font-size: 17px; font-weight: 600; color: #1e293b; margin: 0 0 4px; }
    .vm-ip { font-size: 13px; color: #64748b; margin-bottom: 24px; }
    .vm-actions { width: 100%; .el-button { width: 100%; } }
  }

  .vm-footer {
    padding: 12px 20px; background: #f8fafc; border-top: 1px solid #f1f5f9;
    font-size: 11px; color: #94a3b8; font-family: ui-monospace, SFMono-Regular;
  }
}

:deep(.vnc-session-dialog) {
  display: flex; flex-direction: column;
  background: #000 !important; border-radius: 12px; overflow: hidden;
  
  .el-dialog__header {
    padding: 0 !important;
    margin: 0 !important;
    height: 50px;
    cursor: move; 
  }

  .el-dialog__body {
    flex: 1; display: flex; flex-direction: column;
    padding: 0 !important;
  }
  
  &.is-fullscreen { border-radius: 0; }
}

.session-header {
  height: 50px; background: #1e293b; padding: 0 20px;
  display: flex; justify-content: space-between; align-items: center;
  
  .session-info {
    display: flex; align-items: center; gap: 10px;
    pointer-events: none; 
    .status-indicator { 
      width: 8px; height: 8px; border-radius: 50%; background: #64748b;
      &.is-connected { background: #10b981; box-shadow: 0 0 8px #10b981; }
    }
    .session-name { color: #f1f5f9; font-size: 14px; font-weight: 500; }
  }
  
  .session-controls {
    pointer-events: auto;
    display: flex; align-items: center; gap: 10px;
    .divider { width: 1px; height: 16px; background: #334155; }
    .el-button { color: #94a3b8; &:hover { color: #fff; } }
  }
}

.session-stage {
  flex: 1; width: 100%; min-height: 600px;
  background: #000; position: relative;
  #wmks-container { width: 100% !important; height: 100% !important; position: absolute; }
}

:deep(.el-dialog__headerbtn) { display: none; }
</style>

这是index.html下面需要引入的内容

    <link rel="stylesheet" href="/WebMKS_SDK_2.2.0/css/wmks-all.css">

    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    
    <script>
        window.jQuery = window.$ = window.jQuery;
        console.log('JQuery Check:', window.jQuery.fn.jquery);
    </script>

    <script src="/jquery-ui.min.js"></script>

    <script src="/WebMKS_SDK_2.2.0/wmks.min.js"></script>

    <script>
        if (window.jQuery && (window.jQuery.ui || window.jQuery.widget)) {
            console.log('Result: jQuery UI is fully loaded and attached to jQuery.');
        } else {
            console.error('Result: jQuery UI failed to attach to window.jQuery!');
        }
    </script>

image.png

从零实现富文本编辑器#9-编辑器文本结构变更的受控处理

作者 WindRunnerMax
2025年12月22日 10:50
先前我们主要处理了浏览器复杂DOM结构的默认行为,以及兼容IME输入法的各种输入场景。在这里关注于处理文本结构性变更行为的处理,主要是针对行级别的操作、文本拖拽操作等,分别处于文本结构结构以及变更。

JavaScript 中 this 的终极解析:从 call、bind 到箭头函数的深度探索

作者 AAA阿giao
2025年12月22日 10:47

引言

在 JavaScript 编程的世界里,this 是一个既基础又令人困惑的概念。它看似简单,却常常在不经意间“背叛”我们的预期;它灵活多变,却又遵循着一套严格的规则。尤其当与 callapplybind 以及 ES6 引入的箭头函数结合时,this 的行为变得更加微妙而强大。

本文将结合代码,对 this 的机制进行一次全面、深入且生动的剖析。我们将逐字引用原始代码片段,还原其技术含义,并通过大量示例揭示背后的原理。无论你是初学者还是资深开发者,相信都能从中获得新的洞见。


一、核心内容

我们可以将this问题拆解为几个核心命题:

  1. this 可以被覆盖
  2. bind 用于定时器中绑定 this,延迟执行
  3. call / apply 可指定 this 指向,并立即运行
  4. that = this 利用作用域链保存 this
  5. 箭头函数没有自己的 this,而是继承父级作用域的 this

接下来,我们将围绕这五点展开详细论述。

源代码链接:lesson_zp/batjtmd/this: AI + 全栈学习仓库


二、this 的默认行为:谁调用,就属于谁

在深入高级技巧前,必须先理解 this默认绑定规则

1. 全局上下文中的 this

在非严格模式下,全局作用域中的 this 指向全局对象(浏览器中是 window,Node.js 中是 global):

console.log(this === window); // true (浏览器)

在严格模式下,全局函数中的 thisundefined

'use strict';
function f() { console.log(this); }
f(); // undefined

2. 对象方法中的 this

当函数作为对象的方法被调用时,this 指向该对象:

const obj = {
  name: 'Alice',
  greet() {
    console.log(this.name);
  }
};
obj.greet(); // "Alice"

但注意:函数一旦脱离对象调用,this 就会丢失

const fn = obj.greet;
fn(); // 非严格模式下输出 undefined(this 指向 window)

这就是为什么我们需要 callbind 等工具来“拯救”迷失的 this


三、call 与 apply:强制指定 this,立即执行!

原理与语法

  • func.call(thisArg, arg1, arg2, ...)
  • func.apply(thisArg, [arg1, arg2, ...])

两者功能相同,区别仅在于参数传递方式。

“call apply 指定this 指向 立即运行”

callapply 的核心作用就是在调用函数的同时,显式指定 this 的值,并立刻执行该函数。

示例演示

function introduce(age) {
  console.log(`I'm ${this.name}, ${age} years old.`);
}

const person = { name: 'Bob' };

introduce.call(person, 25);   // I'm Bob, 25 years old.
introduce.apply(person, [30]); // I'm Bob, 30 years old.

即使 introduce 不是 person 的方法,我们也能让它“假装”是——这就是 this 的灵活性。

实际应用场景

  • 数组方法借用(如将类数组转为真数组):

    const args = Array.prototype.slice.call(arguments);
    
  • 通用工具函数适配不同上下文。


四、bind:永久绑定 this,延迟执行

原理与语法

func.bind(thisArg, arg1, arg2, ...) 返回一个新函数,该函数的 this 被永久绑定到 thisArg,且可预设部分参数(柯里化)。

“bind 定时器this绑定(a) 以后再执行”

这句话精准指出了 bind 的两大特点:

  1. 常用于定时器等异步回调中绑定 this
  2. 不立即执行,而是返回一个可稍后调用的函数

经典问题:setTimeout 中的 this 丢失

const timer = {
  seconds: 0,
  start() {
    setInterval(function() {
      this.seconds++; // ❌ this 指向 global/window
      console.log(this.seconds);
    }, 1000);
  }
};
timer.start(); // 输出 NaN(因为 window.seconds 未定义)

解决方案一:使用 bind

const timer = {
  seconds: 0,
  start() {
    setInterval(function() {
      this.seconds++;
      console.log(this.seconds);
    }.bind(this), 1000); // ✅ 绑定外层 this
  }
};
timer.start(); // 1, 2, 3...

这里,.bind(this) 创建了一个新函数,其 this 永久指向 timer 对象,无论何时被调用都不会改变。

解决方案二:that = this(闭包保存)

“that = this; 作用域链保存this且指向”

这是 ES5 时代的经典写法:

const timer = {
  seconds: 0,
  start() {
    const that = this; // 保存 this 到变量
    setInterval(function() {
      that.seconds++; // 通过闭包访问
      console.log(that.seconds);
    }, 1000);
  }
};

虽然有效,但需要额外变量,且在深层嵌套中容易混乱。bind 更加声明式和安全。


五、箭头函数:没有 this 的“佛系”函数

核心特性

箭头函数(Arrow Function)没有自己的 this。它不会创建新的 this 上下文,而是词法地继承外层作用域的 this

“Cherry箭头函数放弃了自己的this,没有this 指向指向 父级作用域的this”

这句话堪称对箭头函数 this 行为的完美概括!

  • “放弃了自己的 this” → 箭头函数内部不存在 this 绑定。
  • “指向父级作用域的 this” → 它的 this 由定义时的外层作用域决定,与调用方式无关。

示例对比

普通函数(this 丢失)

const obj = {
  name: 'David',
  delayedLog() {
    setTimeout(function() {
      console.log(this.name); // ❌ undefined
    }, 100);
  }
};
obj.delayedLog();

箭头函数(自动继承)

const obj = {
  name: 'Cherry', // 注意:原文特意用了 "Cherry"
  delayedLog() {
    setTimeout(() => {
      console.log(this.name); // ✅ "Cherry"
    }, 100);
  }
};
obj.delayedLog();

为什么能成功?因为箭头函数的 this 继承自 delayedLog 方法,而 delayedLogthis 指向 obj

箭头函数的不可变性

由于箭头函数没有 this,所以以下操作无效:

const fn = () => console.log(this.name);
const obj = { name: 'Eve' };

fn.call(obj);   // 仍输出全局作用域的 name(或 undefined)
fn.bind(obj)(); // 同上

callapplybind 对箭头函数的 this 毫无影响

适用场景 vs 陷阱

适合

  • 回调函数(如 map, filter, setTimeout
  • 避免 that = this 的样板代码

不适合

  • 对象方法(会继承外层 this,可能不是对象本身)
  • 构造函数(箭头函数不能用 new 调用)
  • 需要动态 this 的场景

六、综合案例:四种方式对比

假设我们有一个计数器对象,需要在 1 秒后打印当前值:

const counter = {
  count: 0,
  
  // 方式1:普通函数 + that = this
  method1() {
    const self = this;
    setTimeout(function() {
      console.log('method1:', self.count);
    }, 1000);
  },

  // 方式2:bind
  method2() {
    setTimeout(function() {
      console.log('method2:', this.count);
    }.bind(this), 1000);
  },

  // 方式3:箭头函数
  method3() {
    setTimeout(() => {
      console.log('method3:', this.count);
    }, 1000);
  },

  // 方式4:call(需立即执行,不适合定时器,但可模拟)
  method4() {
    const fn = function() { console.log('method4:', this.count); };
    setTimeout(() => fn.call(this), 1000); // 结合箭头+call
  }
};

counter.method1(); // 0
counter.method2(); // 0
counter.method3(); // 0
counter.method4(); // 0

四种方式都能正确输出,但箭头函数最简洁bind 最显式that = this 最传统


七、this 绑定优先级总结

当多个规则同时存在时,JavaScript 按以下优先级确定 this

  1. new 绑定(构造函数)→ 最高
  2. 显式绑定call / apply / bind
  3. 隐式绑定(对象方法调用,如 obj.fn()
  4. 默认绑定(独立函数调用)

箭头函数不参与此规则,因为它根本没有 this


八、常见误区与最佳实践

误区1:认为箭头函数总是更好

错!箭头函数在对象方法中会导致 this 指向外层(可能是全局):

const badObj = {
  name: 'Bad',
  getName: () => this.name // ❌ this 指向全局
};
console.log(badObj.getName()); // undefined

应使用普通函数:

const goodObj = {
  name: 'Good',
  getName() { return this.name; } // ✅
};

误区2:bind 后还能被 call 覆盖

不能!bind 创建的函数其 this永久锁定的:

function f() { console.log(this.x); }
const bound = f.bind({x: 1});
bound.call({x: 2}); // 输出 1,不是 2!

最佳实践

  • 在类方法或对象方法中,使用普通函数。
  • 在回调、事件监听、定时器中,优先考虑箭头函数。
  • 若需兼容旧环境或需要预设参数,使用 bind
  • 避免过度使用 that = this,除非在不支持箭头函数的环境中。

九、结语:掌握 this,就是掌握 JavaScript 的灵魂

现在,我们不仅读懂了它,更理解了其背后的技术哲学:

  • call/apply 是“命令式”的干预——我要你现在就用这个 this
  • bind 是“防御式”的设计——无论何时调用,都必须用这个 this
  • that = this 是“妥协式”的智慧——既然你靠不住,我就自己存一份!
  • 箭头函数 是“声明式”的优雅——我不需要 this,我信任我的上下文!

JavaScript 的魅力,正在于这种灵活性与规则性的统一。当你能自如地在这些工具之间切换,你就不再是 this 的奴隶,而是它的主人。

愿你在未来的代码中,不再对 this 感到迷茫,而是微笑着说:

“我知道你是谁,也知道你该去哪。”

❌
❌