普通视图

发现新文章,点击刷新页面。
昨天 — 2025年11月26日首页

双座敞篷传奇落幕,宝马 Z4 即将停产

作者 芥末
2025年11月26日 22:00

继丰田宣布 GR Supra 停产之后,其「孪生兄弟」宝马 Z4 也迎来了终章。

近日,宝马确认将在 2026 年春季正式结束 Z4(G29 世代)的生产。自 1995 年 Z3 问世以来,宝马持续近三十年的双座敞篷跑车产品线,或将就此画上句号。

回顾 Z 系列的历史,几乎就是一部宝马对驾驶乐趣的执着书写。

1995 年,Z3 凭借圆润线条和詹姆斯·邦德电影《黄金眼》一炮而红;2002 年推出的 Z4(E85 ) 则转向锋利设计,并诞生了搭载 3.2 升直六引擎、输出 336 马力的 Z4 M Roadster——至今仍被不少车迷奉为经典。

▲ 2007 BMW Z4 M Roadster

2009 年的第二代 Z4(E89)首次采用折叠硬顶,试图兼顾日常实用性;而 2018 年回归的第三代 Z4(G29),则重新拥抱轻量化的织物软顶,并与丰田联合开发,共享平台与动力总成,催生出如今广为人知的 GR Supra。

▲ 采用折叠硬顶的 BMW Z4 E89

事实上,早在 2022 年,坊间便有传闻称 Z4 将在当前一代生命周期结束后停产。尽管宝马决定让这款车多延续一个年款,但始终未公布任何换代计划。

Z4 之所以能「续命」至今,很大程度上要归功于手动挡的坚持。宝马原本认为这种传统配置在当今市场已无多少受众,但美国用户的实际选择给出了截然不同的答案:只要有手动挡,就仍有人愿意为它买单。

作为告别之作,宝马最后为 Z4 打造了一款名为「Final Edition」的限量特别版,将将高配选项整合为出厂即满配的状态,让最后一台 Z4 拥有最完整的姿态。

Z4 Final Edition 将全系标配宝马 Individual 部门调制的 Frozen Black 哑光黑车漆,搭配 M Shadowline 高光黑套件和红色 M Sport 刹车卡钳,以及熏黑格栅、外后视镜、进气口及排气尾喉,软顶则采用 Moonlight Black 深灰黑色,试图营造一种低调而凌厉的告别氛围。

车辆内饰则以黑色 Vernasca 真皮与 Alcantara 材质为主调,辅以贯穿仪表台、中控、门板、座椅乃至脚垫的红色缝线,专属门槛饰板刻有「Final Edition」字样。

配置方面,Harman Kardon 音响、抬头显示、环境氛围灯、驾驶辅助系统等此前需额外选装的高端功能,全部作为标准装备提供。

不过,这款特别版仅基于顶配 M40i 打造,将由奥地利麦格纳斯太尔工厂在 2026 年 2 月至 4 月间小批量生产,主要在欧洲和北美市场提供。

Z4 Final Edition 将继续搭载 3.0 升涡轮增压直列六缸发动机,最大输出 382 马力、500 牛·米扭矩,用户可在八速 Steptronic 自动变速箱与六速手动变速箱之间选择。

选择手动挡的用户将获得「Edition Handschalter」专属套件,包含重新调校的减震器阻尼、转向逻辑、牵引力控制系统,以及强化型防倾杆支架,这些细节虽不提升账面性能,却能显著增强人车沟通感,满足纯粹驾驶爱好者的需求。

自动挡车型虽无缘这套底盘优化,却可以获得前后轮尺寸不同的「Staggered」轮圈设定(前 19 英寸/后 20 英寸),此前这一设定在普通 M40i 上仅限手动挡可选。

性能表现上,Z4 Final Edition 自动挡零百加速为 3.9 秒,手动挡为 4.2 秒。

在北美市场,Z4 Final Edition 起售价为 78,675 美元,相比普通版 M40i 的 69,575 美元高出约 9,100 美元。

但若将 Final Edition 所含的 Individual 哑光漆、Shadowline 套件、手动挡专属底盘调校、Premium 包、驾驶辅助包等逐一加到普通 M40i 上,最终价格将与 Final Edition 相差无几。

目前,宝马中国官网的 Z4 车型并未下架,sDrive 25i M 运动曜夜套装的指导价为 52.39 万元,有 197 马力和 340 马力两种动力版本可选,不过仅提供仅 8 速运动型手自一体变速箱。

Z4 的停产,标志着一个时代的落幕。过去三十年,它承载了无数人对敞篷、后驱、直六引擎的浪漫想象。但在当下,这类小众跑车在全球市场的销量始终有限,Z4 与 Supra 的联合项目,也从未真正「卖爆」。

不过,宝马并未彻底关闭跑车的大门。一位高管近期向媒体透露,基于全新「Neue Klasse」(新世代)纯电平台开发一款全新运动车型「是可行的」。

▲ 网友渲染的 「Neue Klasse」版 Z4

这意味着,Z4 的精神或许将以纯电身份延续,只是不再与丰田共享,也不再是那台熟悉的燃油敞篷。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


北京启动专项行动重点整治六类金融领域网络乱象

2025年11月26日 21:03
36氪获悉,据网信北京消息,当前,部分金融领域“黑灰产”借助网络平台滋生蔓延,通过假冒身份、隐蔽引流、非法推介等手段误导公众、欺诈敛财,严重侵害人民群众财产安全,扰乱金融市场秩序。为有效遏制金融领域突出网络乱象,11月26日,北京市网信办会同北京金融监管局联合部署开展为期3个月的“清朗京华·金融守护”金融领域网络乱象治理专项行动,以高压态势严打网上非法金融活动及“黑灰产”乱象。此次专项行动聚焦短视频、社交、直播等重点网站平台,集中整治假冒仿冒专业人员误导公众、以隐蔽方式违规引流、为非法存贷款中介提供推介服务、开展非法代理维权“黑灰产”活动、恶意抹黑诋毁金融机构、捏造虚假信息唱衰经济等六类突出问题。

商务部:敦促荷兰政府尽快提出建设性的解决方案,为企业开展内部协商创造有利条件

2025年11月26日 21:01
36氪获悉,据商务部官网,11月26日,商务部部长王文涛与欧盟委员会贸易和经济安全委员谢夫乔维奇举行视频会谈,就安世半导体等经贸问题深入交换意见。王文涛表示,关于安世半导体问题,造成当前全球半导体产供链混乱的源头和责任在荷兰方面。中国政府始终本着负责任的态度,及时采取切实措施,努力维护和恢复全球半导体产供链稳定。近期,荷方宣布暂停行政令,向妥善解决问题迈出了一小步,但是荷方对企业的不当行政和司法干预仍未取消,全球半导体产供链尚未恢复正常,依然面临较大不确定性。希望欧方发挥积极作用,敦促荷兰政府尽快提出建设性的解决方案,为企业开展内部协商创造有利条件。

德意志银行上调2026年金价预期

2025年11月26日 20:48
德意志银行上调了其对明年金价的预测,理由是持续的投资资金流入和各国央行坚挺的需求。该行目前预计,黄金均价将达到每金衡盎司4450美元,高于此前预期的4000美元,并预测金价将在3950美元至4950美元之间波动。(新浪财经)

北大医药:相关流感药物尚未组织生产销售

2025年11月26日 20:45
36氪获悉,北大医药发布股票交易异常波动的公告,公司注意到近期部分地区可能存在流感高发情况,引发资本市场关注,经自查,公司相关流感药物尚未组织生产销售,预计未来相当长一段时间不会对公司经营业绩产生相关影响。

南方精工:股东拟合计减持不超过2.54%公司股份

2025年11月26日 20:40
36氪获悉,南方精工公告,公司收到控股股东一致行动人浙江银万私募基金管理有限公司-银万全盈30号私募证券投资基金和史建仲的减持计划告知函。浙江银万私募基金管理有限公司-银万全盈30号私募证券投资基金持有公司1.97%的股份,史建仲持有0.57%的股份,计划在未来三个月内通过集中竞价及大宗交易方式合计减持不超过8,957,230股,即不超过公司总股本的2.54%。减持原因为自身资金需求。

大道至简-Shadcn/ui设计系统初体验(下):Theme与色彩系统实战

作者 ArkPppp
2025年11月26日 18:51

大道至简-Shadcn/ui设计系统初体验(下):Theme与色彩系统实战

前言

在上篇文章中,我们探讨了shadcn/ui的安装、组件引入和基础定制。本文将继续深入,关注一个更核心的话题——主题系统设计。作为前端工程师,我们都明白一个好的设计系统不仅要有美观的组件,更需要一套完整、可维护、可扩展的色彩体系。本文将通过实际项目实践,详细分析shadcn/ui如何通过CSS变量和TailwindCSS构建这套体系。

一、自定义主题配置:从CSS变量到TailwindCSS

1.1 shadcn/ui的主题系统原理

shadcn/ui采用了基于CSS自定义属性(CSS Variables)的设计模式。每个主题实际上就是一套CSS变量的集合。不同于传统组件库通过JavaScript动态计算颜色值,shadcn/ui选择在CSS层面定义好所有颜色状态,然后通过类名切换来实现主题变换。

这种设计的优势显而易见:

  • 无需JavaScript计算,避免频繁的重排重绘
  • 颜色值在构建阶段就已经确定,性能更好
  • CSS变量天然支持继承和级联,便于管理复杂的色彩体系

让我们查看项目的核心配置文件:

/* src/index.css */
@import "tailwindcss";

@custom-variant dark (&:is(.dark *));

@theme {
  --color-border: hsl(var(--border));
  --color-input: hsl(var(--input));
  --color-ring: hsl(var(--ring));
  --color-background: hsl(var(--background));
  --color-foreground: hsl(var(--foreground));
  /* ... 更多颜色变量 */
}

注意这里使用TailwindCSS 4的新语法 @theme 替代了传统的 tailwind.config.js 配置。这种方式将主题配置直接内联到CSS文件中,更加直观。

1.2 主题色彩定义

shadcn/ui使用HSL色彩空间来定义颜色。HSL由色相(Hue)、饱和度(Saturation)、亮度(Lightness)三个分量组成,相比RGB更容易理解和调整。

我们项目的实际配色:

:root {
  /* 背景与前景色 */
  --background: 210 20% 96%;
  --foreground: 222 15% 15%;

  /* 主色调 - 浅蓝色系 */
  --primary: 205 85% 60%;
  --primary-foreground: 210 40% 98%;

  /* 次要色 - 浅绿色系 */
  --secondary: 145 65% 60%;
  --secondary-foreground: 222 15% 15%;

  /* 强调色 - 青绿色系 */
  --accent: 175 70% 55%;
  --accent-foreground: 210 40% 98%;
}

.dark {
  /* 深色主题配色 */
  --background: 210 15% 10%;
  --foreground: 210 15% 92%;
  --primary: 205 85% 65%;
  --secondary: 145 60% 65%;
  --accent: 175 65% 60%;
  /* ... */
}

配色方案的设计遵循以下原则:

  1. 语义化命名:每个颜色都有明确的语义(background、primary、secondary等)
  2. 状态配套:每个主要颜色都有对应的foreground色,保证可读性
  3. 明暗适配:深色模式下适当调整亮度和饱和度

1.3 扩展色系:成功与警告

除了标准的设计语言色彩,shadcn/ui还允许定义扩展色系,用于表达特定状态:

:root {
  --success: 145 60% 50%;
  --success-light: 145 65% 60%;
  --success-dark: 145 55% 45%;

  --warning: 45 85% 60%;
  --warning-light: 45 90% 65%;
  --warning-dark: 45 80% 55%;
}

这种命名方式(基础色-light-dark)为每个语义色提供了三个亮度级别,在实际开发中可以根据不同场景选择合适的深浅。

二、颜色系统设计:CSS变量与OKLCH色彩空间

2.1 CSS变量的高级特性

CSS自定义属性(CSS Variables)不仅仅是简单的键值对,它具备许多强大的特性:

1. 继承性

.card {
  background: hsl(var(--primary));
}

.card-header {
  /* 自动继承父元素的 --primary */
  color: hsl(var(--primary));
}

2. 动态计算

:root {
  --primary-light: 205 85% calc(60% + 10%);
}

3. 作用域控制

/* 全局作用域 */
:root {
  --global-primary: blue;
}

/* 局部作用域 */
.theme-dark {
  --local-primary: red;
}

这些特性使得CSS变量非常适合构建复杂的颜色系统。

2.2 OKLCH色彩空间:下一代色彩标准

传统的HSL色彩空间有一个明显缺陷:感知不均匀性。也就是说,在HSL中同样数值的变化,人眼感知的差异并不一致。例如,HSL中饱和度从50%到60%的变化,看起来比60%到70%的变化更明显。

OKLCH(Lightness-Chroma-Hue)色彩空间解决了这个问题。OKLCH是基于CIELAB色彩空间的现代色彩模型,具有以下优势:

  • 感知均匀:数值的微小变化对应人眼感知的微小变化
  • 色域更广:支持更多可见色彩
  • 对比度可控:更容易满足WCAG可访问性标准

虽然浏览器对OKLCH的支持还在逐步完善中,但TailwindCSS已经开始采用OKLCH。未来shadcn/ui很可能会迁移到OKLCH色彩空间。

2.3 构建语义化颜色系统

一个好的颜色系统需要避免直接使用底层色彩值,而是通过语义化变量来使用:

/* ❌ 不好的做法 - 直接使用底层颜色 */
.button {
  background: rgb(59, 130, 246);
}

/* ✅ 好的做法 - 使用语义化变量 */
.button {
  background: hsl(var(--primary));
}

这种设计的好处:

  1. 可维护性强:修改主题时只需更改CSS变量定义
  2. 一致性保证:全站使用统一的语义化色彩
  3. 灵活性高:可以针对不同区域覆盖特定变量

三、项目实践:TodoList的主题更新实现

3.1 ThemeProvider设计

shadcn/ui提供了一个独立的ThemeProvider实现,位于 src/components/theme-provider.tsx。这个实现替代了传统的next-themes,更轻量且完全基于原生Web API。

核心实现分析:

export function ThemeProvider({
  children,
  defaultTheme = 'system',
  storageKey = 'vite-ui-theme',
}: ThemeProviderProps) {
  const [theme, setTheme] = useState<Theme>(
    () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
  )

  useEffect(() => {
    const root = window.document.documentElement

    root.classList.remove('light', 'dark')

    if (theme === 'system') {
      const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
        .matches
        ? 'dark'
        : 'light'

      root.classList.add(systemTheme)
      return
    }

    root.classList.add(theme)
  }, [theme])

  const value = {
    theme,
    setTheme: (theme: Theme) => {
      localStorage.setItem(storageKey, theme)
      setTheme(theme)
    },
  }

  return (
    <ThemeProviderContext.Provider {...props} value={value}>
      {children}
    </ThemeProviderContext.Provider>
  )
}

关键点分析:

  1. 三种主题模式

    • light: 强制使用浅色主题
    • dark: 强制使用深色主题
    • system: 跟随系统设置
  2. 本地存储持久化 使用localStorage保存用户偏好,应用重启后自动恢复。

  3. 类名切换机制 通过操作documentElement的classList来切换主题,避免频繁的style重写。

3.2 TodoList中的主题切换按钮

在TodoList组件中,主题切换按钮的实现:

import { useTheme } from './theme-provider'

function TodoList() {
  const { theme, setTheme } = useTheme()

  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light')
  }

  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={toggleTheme}
      aria-label="切换主题"
    >
      {theme === 'light' ? (
        <Moon className="h-4 w-4" />
      ) : (
        <Sun className="h-4 w-4" />
      )}
    </Button>
  )
}

注意这里的实现细节:

  • 使用aria-label提升可访问性
  • 根据当前主题显示对应图标(月亮/太阳)
  • variant设为ghost保持视觉简洁

3.3 主题变量的实际应用

在TodoList组件中,我们看到各种shadcn/ui组件都使用了语义化的颜色变量:

<div className="min-h-screen bg-background text-foreground transition-colors">
  <Card className="border-border">
    <CardHeader>
      <CardTitle className="bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent">
        待办事项列表
      </CardTitle>
    </CardHeader>
  </Card>
</div>

关键点:

  • bg-backgroundtext-foreground:使用语义变量确保文本可读性
  • border-border:边框颜色随主题变化
  • 渐变色使用CSS变量,保持主题一致性

四、shadcn/ui的设计哲学总结

通过上下两篇文章的分析,我们可以总结shadcn/ui的设计哲学:

4.1 零抽象成本

shadcn/ui不将组件封装为黑盒,而是提供完整源代码。这种"代码所有权"模式让开发者可以:

  • 任意修改组件实现
  • 深入理解组件逻辑
  • 无框架依赖,便于迁移

4.2 原子化设计

每个组件都是独立的、无样式基础的(headless),样式完全通过TailwindCSS类控制。这带来:

  • 样式完全可控
  • 避免CSS优先级冲突
  • 更好的Tree-shaking效果

4.3 设计令牌驱动

通过CSS变量系统,shadcn/ui建立了完整的设计令牌(Design Tokens)体系:

  • 颜色、字体、间距等都有对应的令牌
  • 令牌支持层级继承
  • 便于实现设计系统的一致性

4.4 可访问性优先

基于Radix UI构建,所有组件都具备:

  • 完整的键盘导航支持
  • 正确的ARIA属性
  • 语义化的HTML结构

4.5 现代化工具链

shadcn/ui深度集成了现代前端工具:

  • TailwindCSS 4(最新语法)
  • TypeScript(完整类型定义)
  • Vite(快速构建)
  • ESLint(代码规范)

5.成果展示

让我们看看最终的成果吧。

LightMode.png

NightMode.png

结语

shadcn/ui不仅仅是一个组件库,更是一套完整的设计系统实现方案。它通过CSS变量、TailwindCSS和现代React模式的结合,为我们提供了一种全新的组件库构建思路。

这种"大道至简"的设计理念——将复杂的UI抽象还原为简单的CSS变量和可组合的组件——或许正是前端开发的一种新范式。在AI编程工具日益成熟,Vibe Coding愈发普遍的今天,一个开放、可定制、无黑盒的组件库将更具生命力。


参考:

栈(Stack)详解:从原理到实现,再到括号匹配应用

作者 www_stdio
2025年11月26日 18:45

栈(Stack)详解:从原理到实现,再到括号匹配应用

栈是一种基础而重要的线性数据结构,在计算机科学中被广泛应用于函数调用、表达式求值、回溯算法等场景。本文将围绕栈的定义、抽象数据类型(ADT)、ES6 实现方式(数组与链表)、性能对比,以及一个经典应用场景——括号匹配问题,进行系统讲解。


一、什么是栈?

栈(Stack)是一种遵循 先进后出(First In Last Out, FILO) 原则的线性数据结构。你可以把它想象成一摞盘子:你只能从顶部放入(入栈)或取出(出栈)盘子,不能从中间或底部操作。

栈的核心操作包括:

  • push(x) :将元素 x 压入栈顶。
  • pop() :弹出并返回栈顶元素。
  • peek() / top() :查看栈顶元素但不移除。
  • isEmpty() :判断栈是否为空。
  • size() :获取栈中元素个数。

二、栈的抽象数据类型(ADT)

抽象数据类型(Abstract Data Type, ADT)是对数据结构行为的规范描述,不涉及具体实现。栈的 ADT 应包含以下属性和方法:

属性/方法 描述
size 只读属性,返回当前栈的大小
isEmpty() 判断栈是否为空
push(val) 入栈操作
pop() 出栈操作,若栈空则抛出异常
peek() 返回栈顶元素,若栈空则抛出异常
toArray() (可选)将栈内容转换为数组,便于调试或输出

三、ES6 Class 实现栈

ES6 引入了 class 语法,使面向对象编程更加清晰。结合私有字段(#)、get 访问器等特性,可以优雅地封装栈的实现细节。

1. 基于链表实现栈(LinkedListStack)

链表天然适合动态增长,每个节点包含值和指向下一个节点的指针。

class ListNode {
    constructor(val) {
        this.val = val;
        this.next = null;
    }
}

class LinkedListStack {
    #stackPeek = null; // 私有栈顶指针
    #size = 0;

    push(num) {
        const node = new ListNode(num);
        node.next = this.#stackPeek;
        this.#stackPeek = node;
        this.#size++;
    }

    pop() {
        if (!this.#stackPeek) throw new Error('栈为空');
        const val = this.#stackPeek.val;
        this.#stackPeek = this.#stackPeek.next;
        this.#size--;
        return val;
    }

    peek() {
        if (!this.#stackPeek) throw new Error('栈为空');
        return this.#stackPeek.val;
    }

    get size() { return this.#size; }
    isEmpty() { return this.#size === 0; }

    toArray() {
        let node = this.#stackPeek;
        const res = new Array(this.#size);
        for (let i = res.length - 1; i >= 0; i--) {
            res[i] = node.val;
            node = node.next;
        }
        return res;
    }
}

优点:动态扩容,无空间浪费;插入/删除均为 O(1)
缺点:每个节点需额外存储指针,内存开销略大


2. 基于数组实现栈(ArrayStack)

利用 JavaScript 数组的 pushpop 方法,可快速实现栈。

class ArrayStack {
    #stack = [];

    get size() { return this.#stack.length; }
    isEmpty() { return this.size === 0; }

    push(num) {
        this.#stack.push(num);
    }

    pop() {
        if (this.isEmpty()) throw new Error("栈为空");
        return this.#stack.pop();
    }

    peek() {
        if (this.isEmpty()) throw new Error("栈为空");
        return this.#stack[this.size - 1];
    }

    toArray() {
        return [...this.#stack]; // 返回副本更安全
    }
}

优点:缓存友好,访问快;代码简洁
缺点:扩容时需复制整个数组(O(n)),但均摊时间复杂度仍为 O(1)


四、数组 vs 链表实现栈:性能对比

维度 数组实现 链表实现
时间效率 平均 O(1),扩容时 O(n) 稳定 O(1)
空间效率 可能有预分配空间浪费 每个节点多一个指针开销
内存布局 连续内存,缓存命中率高 离散内存,缓存局部性差
适用场景 数据量可预估、追求速度 动态性强、内存敏感

💡 在大多数实际应用中,数组实现的栈更常用,因为其简单高效,且现代 JavaScript 引擎对数组优化极佳。


五、实战应用:括号匹配问题

栈的经典应用场景之一是验证括号字符串是否合法。例如:"([{}])" 合法,而 "([)]" 不合法。

解题思路:

  1. 遇到左括号 ([{,将其对应的右括号压入栈;
  2. 遇到右括号,检查是否与栈顶元素匹配;
  3. 若不匹配或栈为空,则非法;
  4. 遍历结束后,栈必须为空才合法。

代码实现:

const leftToRight = {
    "(": ")",
    "[": "]",
    "{": "}"
};

function isValid(s) {
    if (!s) return true;
    const stack = [];
    for (const ch of s) {
        if (ch in leftToRight) {
            stack.push(leftToRight[ch]); // 压入期望的右括号
        } else {
            if (!stack.length || stack.pop() !== ch) {
                return false; // 不匹配或栈空
            }
        }
    }
    return stack.length === 0; // 必须完全匹配
}

✅ 时间复杂度:O(n)
✅ 空间复杂度:O(n)(最坏情况全为左括号)


六、总结

  • 栈是一种 FILO 的线性结构,核心操作为 pushpoppeek
  • ES6 的 class、私有字段 #get 访问器,让栈的实现更安全、更清晰。
  • 数组实现简单高效,适合大多数场景;链表实现动态灵活,适合不确定规模的场景。
  • 栈在算法中用途广泛,如括号匹配、表达式求值、深度优先搜索(DFS)等。

掌握栈,不仅是理解数据结构的第一步,更是打开算法世界大门的钥匙。


📌 提示:在实际开发中,除非有特殊需求(如限制使用内置数组方法),否则直接使用 Arraypush/pop 即可高效模拟栈行为。

前端日常工作开发技巧汇总

2025年11月26日 18:41

一、JS篇

1. structuredClone 深拷贝

JavaScript 内置了一个 structuredClone() 的方法, 此方法提供了一种简单有效的方法来深度克隆对象,支持复杂数据类型,包括 DateRegExpMapSetArrayBufferBlobFile 等。浏览器底层实现,通常比手动递归或 JSON 方法更高效。

兼容性 image.png

2. 函数式编程

ES14 更新了许多数组方法或者为原有的数组方法增加不会带来突变(without mutation) 的互补方法。意味着它们会基于原数组创建新的数组,而不是直接修改原数组。

新增的互补方法有

  • Array.sort() -> Array.toSorted()
  • Array.splice() -> Array.toSpliced()
  • Array.reverse() -> Array.toReversed()

新增的新数组方法有:Array.with()Array.findLast()Array.findLastIndex()

  • Array.with()
    返回一个新数组,将原数组中指定索引 index 的元素替换为 value不修改原数组

语法
index:要替换的元素的索引(可以是负数,表示从末尾开始计数)。
value:替换后的新值

const newArray = array.with(index, value)

const arr = [1, 2, 3, 4];
const newArr = arr.with(1, "hello"); // 替换索引 1 的元素 

console.log(arr);    // [1, 2, 3, 4](原数组不变)
console.log(newArr); // [1, "hello", 3, 4](新数组)

// 支持负数索引(从末尾开始)
const newArr2 = arr.with(-2, "world"); // 替换倒数第 2 个元素
console.log(newArr2); // [1, 2, "world", 4]
  • Array.findLast()
    从数组末尾向前查找第一个满足 callback 条件的元素,并返回该元素。如果未找到,返回 undefined

  • Array.findLastIndex()
    从数组末尾向前查找第一个满足 callback 条件的元素,并返回其索引。如果未找到,返回 -1

3. 惰性函数

JavaScript 中的 惰性函数(Lazy Function) 是一种优化技术,其核心思想是:函数在第一次调用时执行一些初始化或判断逻辑,并在执行后将自身重定义为一个更高效或更简单的版本,后续调用就直接使用这个新版本,避免重复开销

普通写法

function copyToClipboard(text) {
    // 优先使用Clipboard API
    if (navigator.clipboard) {
      return navigator.clipboard
        .writeText(text)
        .then(() => {
          Message.success('复制成功')
          return true
        })
        .catch((err) => {
          console.error('使用Clipboard API复制失败: ', err)
          // 如果Clipboard API失败,尝试使用降级方案
          return copyUsingExecCommand(text)
        })
    } else {
      // 如果不支持Clipboard API,直接使用降级方案
      return copyUsingExecCommand(text)
    }
}

惰性写法

function copyToClipboard(text) {
  // 第一次调用时进行能力检测,并重定义自身
  if (navigator.clipboard) {
    // 支持 Clipboard API
    copyToClipboard = function (text) {
      return navigator.clipboard
        .writeText(text)
        .then(() => {
          console.log('文本已成功复制到剪贴板');
          return true;
        })
        .catch((err) => {
          console.error('Clipboard API 复制失败:', err);
          return false;
        });
    };
  } else {
    // 不支持 Clipboard API,使用 execCommand 降级方案
    copyToClipboard = function (text) {
      return copyUsingExecCommand(text);
    };
  }

  // 执行第一次调用
  return copyToClipboard(text);
}

二、CSS篇

1. 滚动吸附

<template>
  <div>
    <div class="container">
      <div class="item">1</div>
      <div class="item">2</div>
      <div class="item">3</div>
    </div>
  </div>
</template>

<script setup name="Snap"></script>

<style lang="scss" scoped>
.container {
  width: 100%;
  height: 300px;
  display: flex;
  overflow-x: scroll;
  // 吸附效果 mandatory: 必须吸附  proximity: 靠近时吸附
  scroll-snap-type: x mandatory;
  .item {
    flex-shrink: 0;
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 30px;
    color: #fff;
    background-color: #ccc;
    // 吸附位置
    scroll-snap-align: start;
    scroll-snap-stop: always;
    &:nth-child(1) {
      background-color: #f56c6c;
    }
    &:nth-child(2) {
      background-color: #67c23a;
    }
    &:nth-child(3) {
      background-color: #409eff;
    }
  }
}
</style>
兼容性较高

image.png

2. 字体自适应容器大小

<template>
  <div>
    <div class="container">
      <p>字体自适应容器大小</p>
    </div>
  </div>
</template>

<script setup name=""></script>

<style lang="scss" scoped>
.container {
  width: 500px;
  height: 300px;
  padding: 15px;
  resize: both;
  overflow: hidden;
  background-color: aquamarine;
  container-type: inline-size; // 启用容器查询 size:基于宽高 / inline-size 基于宽度 / normal 不启用
  p {
    font-size: 5cqh;
  }
}
</style>
兼容性还行

image.png

3. 选择器

  • 选择器特定性(通常叫做选择器权重)

当希望某个css属性优先级高于其他属性值时,尽量不要使用!important!important会打破这些固有的级联规则,使得样式的应用变得不那么可预测。这可能会导致样式表难以维护和理解,尤其是在大型项目中。增加调试难度,也限制了样式的灵活性。

替代方案: 通过编写更具体(或更精确)的选择器来覆盖样式,或者叠加选择器,比如:222

.el-button.el-button {
    color: red;
}
  • 新型选择器

:has()选择器: 根据一个元素是否包含某些特定的后代元素,或者其后的同级元素是否满足某些条件,来选中该元素本身。这实现了“向下”观察的能力。

示例1: 选择包含 <img><div>

<div>这个 div 没有图片,不会被选中</div>
<div>
    <img src="example.jpg" alt="示例图片">
    这个 div 包含图片,会被红色边框包围
</div>
/* 选择包含 <img> 的 div */ 
div:has(img) {
    border: 3px solid red; padding: 10px;
}

示例2: 选择紧跟着 <p><h2>

<h2>这个 h2 后面没有紧跟着 p,不会被选中</h2>
<div>分隔内容</div>
<h2>这个 h2 后面紧跟着 p</h2>
<p>这个 p 是 h2 的紧邻兄弟元素,因此 h2 会变成蓝色斜体</p>
/* 选择后面紧跟着 <p> 的 h2 */
h2:has(+ p) {
    color: blue; font-style: italic;
}

兼容性 image.png

:is()选择器: 它接受一个逗号分隔的选择器列表作为参数,并匹配其中任意一个选择器。这有助于减少冗余代码,提高可读性。:is() 的权重等于它括号里所有选择器中权重最高的那个。

示例:

<header>
    <h1>这是 header 的 h1(紫色)</h1>
</header>
<main>
    <h1>这是 main 的 h1(紫色)</h1>
</main>
<footer>
    <h1>这是 footer 的 h1(紫色)</h1>
</footer>
<section>
    <h1>这个 h1 不在 :is() 范围内,保持默认颜色</h1>
</section>
/* 统一设置 header、main、footer 下的 h1 样式 */
:is(header, main, footer) h1 {
    color: purple; font-family: Arial, sans-serif;
}

兼容性 image.png

:where()选择器: 与 :is() 类似,但权重永远为 0,适合默认样式。

兼容性 image.png

三、VUE篇

1. v-memo

Vue 3 提供的性能优化指令,其作用是通过缓存模板子树的渲染结果,仅在依赖项变化时重新渲染,从而减少不必要的虚拟 DOM 计算和更新操作。

v-memo 接收一个依赖数组,只有当数组中的值发生变化时才会重新渲染。

示例:优化大型列表渲染,避免全量更新。 当 item.id 或 item.status 变化时,仅更新对应项;其他项复用缓存结果

<div v-for="item in list" :key="item.id" v-memo="[item.id, item.status]">
  {{ item.content }}
  <StatusBadge :status="item.status" />
</div>

2. watch —— 副作用和深度监听

3. customRef ——— 自定义响应式依赖追踪

4. 组件的二次封装

四、Chrome浏览器调试技巧

1. $0

2. 模拟聚焦网页

3. 重放XHR

五、VSCode编辑器插件分享

1. i18n Ally

  • 代码内直接预览翻译文本
  • 快速生成初始翻译
  • 一键跳转至对应翻译条目
  • 集中管理
  "i18n-ally.localesPaths": ["./src/i18n/lang/locales"], // 翻译文件夹路径
  "i18n-ally.pathMatcher": "{locale}/**/{namespace}.json", // 翻译目标文件路径匹配
  "i18n-ally.keystyle": "nested", // 翻译路径格式,
  "i18n-ally.sourceLanguage": "zh-CN", // 翻译源语言
  "i18n-ally.displayLanguage": "zh-CN", //显示语言, 这里也可以设置显示英文为en
  "i18n-ally.sortKeys": true, // 是否自动排序
  "i18n-ally.namespace": false, // 是否启用命名空间,一般在积攒多个待翻译文案时启用,可以自动编辑至对应文件中
  "i18n-ally.enabledParsers": ["ts", "js", "json"], // 翻译文件可允许的格式,默认json

2. koroFileHeader @4.9.2

用于生成文件头部注释和函数注释的插件

快捷键‌:

  • 头部注释:Ctrl+Win+I(Windows/Linux)或 Ctrl+Cmd+I(Mac)
  • 函数注释:Ctrl+Win+T(Windows/Linux)或 Ctrl+Cmd+T(Mac)
// 头部注释
"fileheader.customMade": {
"Author": "git config user.name && git config user.email", // 同时获取用户名与邮箱
"Date": "Do not edit", // 文件创建时间
"LastEditors": "git config user.name && git config user.email", // 文件最后编辑者 与Author字段一致
"LastEditTime": "Do not edit", // 文件最后编辑时间
"FilePath": "Do not edit", // 文件在项目中的相对路径 自动更新
"Description": "" // 文件描述
},

通义千问Qwen3-Coder模型帮我总结的typescript笔记

作者 炒米2333
2025年11月26日 18:31

简单学了学typescript,但是又觉得做笔记麻烦,于是找AI帮帮忙,提示词为“帮我总结typescript的常用知识点,并生成一篇markdown。” 生成了下面的内容。


TypeScript 常用知识点速查

TypeScript (TS) 是 JavaScript 的超集,添加了静态类型系统,有助于提高代码质量和开发效率。以下是其核心和常用的特性总结。

目录

  1. 基础类型
  2. 变量声明与类型注解
  3. 接口 (Interfaces)
  4. 类型别名 (Type Aliases)
  5. 联合类型 (Union Types) & 交叉类型 (Intersection Types)
  6. 数组与元组 (Tuples)
  7. 函数类型
  8. 类 (Classes)
  9. 泛型 (Generics)
  10. 模块 (Modules) 和 导入/导出
  11. 装饰器 (Decorators)
  12. 实用工具类型 (Utility Types)

基础类型

TypeScript 提供了一系列基础数据类型:

  • number: 数字类型 (整数或浮点数)。
  • string: 字符串类型。
  • boolean: 布尔值 (truefalse)。
  • nullundefined: 它们有自己的类型 nullundefined。但在 strictNullChecks 模式下(推荐),它们只能赋值给 any 和各自的类型;否则(非严格模式),它们是所有类型的子类型。
  • symbol: ES6 新增的原始数据类型,表示独一无二的值。
  • bigint: 大整数类型。
  • void: 通常用于表示没有返回值的函数的返回类型。
  • any: 允许任何类型的值,会跳过类型检查(不推荐滥用)。
  • unknown: 代表任何值。与 any 不同的是,在对 unknown 类型的值执行操作前,必须进行类型检查或类型断言。
  • never: 表示永不存在的值的类型。例如,总是抛出异常或根本不可能有返回值的函数表达式的返回类型。
  • object: 非原始类型(即除 number, string, boolean, symbol, null, undefined, bigint 之外的类型)。注意:它不同于 {}

变量声明与类型注解

使用 let, const, var 声明变量,并通过冒号 : 添加类型注解。

let isDone: boolean = false;
let decimal: number = 6;
let color: string = "blue";
let list: number[] = [1, 2, 3];
let u: undefined = undefined;
let n: null = null;

// 函数参数和返回值的类型注解
function add(x: number, y: number): number {
    return x + y;
}

接口 (Interfaces)

接口用于定义对象的结构(Shape),是一种契约。

interface Person {
    name: string;
    age: number;
    address?: string; // 可选属性
    readonly id: number; // 只读属性
}

const user: Person = {
    name: "Alice",
    age: 30,
    id: 1
};
// user.id = 2; // Error: Cannot assign to 'id' because it is a read-only property.

// 函数类型接口
interface SearchFunc {
    (source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(src, sub) { // 参数名不必与接口中定义的名字相匹配
    let result = src.search(sub);
    return result > -1;
}

类型别名 (Type Aliases)

类型别名为类型创建一个新的名称。它可以用于原始值、联合类型、元组以及其它任何你需要手写的类型。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;

function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    } else {
        return n();
    }
}

// 也可以像接口一样描述对象形状
type Point = {
    x: number;
    y: number;
};

// 类型别名可以使用交集和联合
type PartialPoint = { x: number; } | { y: number; };

接口 vs 类型别名:

  • 接口可以“打开”并扩展(Declaration Merging)。
  • 类型别名不能被重新打开以添加新的属性。
  • 接口通常用于定义对象的结构,而类型别名更通用。

联合类型 (Union Types) & 交叉类型 (Intersection Types)

  • 联合类型: 表示一个值可以是几种类型之一。使用 | 分隔每个类型。

    let value: string | number;
    value = "hello"; // OK
    value = 42;      // OK
    // value = true; // Error
    
  • 交叉类型: 将多个类型合并为一个类型。使用 & 连接。

    interface Colorful {
        color: string;
    }
    interface Circle {
        radius: number;
    }
    
    type ColorfulCircle = Colorful & Circle;
    
    const cc: ColorfulCircle = {
        color: "red",
        radius: 10
    }; // 必须同时满足 Colorful 和 Circle 的要求
    

数组与元组 (Tuples)

  • 数组: 存储相同类型的元素。

    let list1: number[] = [1, 2, 3];
    let list2: Array<number> = [1, 2, 3]; // 泛型语法
    
  • 元组: 允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。

    let tuple: [string, number] = ['hello', 10];
    // tuple[0] = 10; // Error: Type 'number' is not assignable to type 'string'.
    

函数类型

  • 函数声明/表达式中的类型注解:

    function greet(name: string): string {
        return "Hello, " + name;
    }
    
    const greeter = function(name: string): string {
         return "Hello, " + name;
    };
    
    const arrowGreeter = (name: string): string => {
        return "Hello, " + name;
    };
    
  • 可选参数和默认参数:

    function buildName(firstName: string, lastName?: string) { ... } // lastName 是可选的
    function buildNameWithDefault(firstName: string, lastName = "Smith") { ... } // 默认参数
    
  • 剩余参数:

    function buildNameRest(firstName: string, ...restOfName: string[]) {
        return firstName + " " + restOfName.join(" ");
    }
    

类 (Classes)

TypeScript 支持面向对象编程的类。

class Animal {
    name: string;
    private age: number; // 私有属性
    protected species: string; // 受保护的属性
    readonly legs: number = 4; // 只读属性

    constructor(theName: string, theAge: number, theSpecies: string) {
        this.name = theName;
        this.age = theAge;
        this.species = theSpecies;
    }

    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Dog extends Animal {
    breed: string;

    constructor(name: string, age: number, species: string, breed: string) {
        super(name, age, species); // 调用父类构造函数
        this.breed = breed;
    }

    bark() {
        console.log('Woof! Woof!');
    }

    move(distanceInMeters = 5) { // 重写父类方法
        console.log("Running...");
        super.move(distanceInMeters);
    }
}

泛型 (Generics)

泛型允许你编写可复用的组件,这些组件可以工作在多种类型之上。

// 泛型函数
function identity<T>(arg: T): T {
    return arg;
}
let output1 = identity<string>("myString"); // type argument specified
let output2 = identity("myString"); // type argument inferred

// 泛型接口
interface GenericIdentityFn<T> {
    (arg: T): T;
}
let myIdentity: GenericIdentityFn<number> = identity;

// 泛型类
class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

// 泛型约束
interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length); // Now we know it has a .length property, so no more error
    return arg;
}
// loggingIdentity(3); // Error, number doesn't have a .length property
loggingIdentity({length: 10, value: 3}); // OK

模块 (Modules) 和 导入/导出

将代码组织到不同的文件中,并控制它们之间的可见性。

// math.ts
export const pi = 3.14;
export function add(x: number, y: number) {
    return x + y;
}
// 或者统一导出
// const pi = 3.14;
// function add(x: number, y: number) { return x + y; }
// export { pi, add };

// main.ts
import { pi, add } from './math';
console.log(pi); // 3.14
console.log(add(1, 2)); // 3

// 导入重命名
import { add as sum } from './math';

// 导入整个模块
import * as math from './math';
console.log(math.pi);

// 默认导出
// export default class Calculator { ... }
// import Calculator from './Calculator'; // 注意没有花括号 {}

装饰器 (Decorators)

一种特殊类型的声明,可以被附加到类声明,方法,访问符,属性或参数上。装饰器使用 @expression 这种形式,expression 求值后必须为一个函数,该函数会在运行时被调用。

需要启用实验性支持 ("experimentalDecorators": true in tsconfig.json)。

// 类装饰器示例
function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

实用工具类型 (Utility Types)

TypeScript 内置了一些非常有用的工具类型来简化常见的类型转换。

  • Partial<T>: 构造类型 T,并将它所有的属性设置为可选的。
  • Readonly<T>: 构造类型 T,并将它所有的属性设置为只读的。
  • Record<K, T>: 构造一个类型,其属性名的类型是 K,属性值的类型是 T
  • Pick<T, K>: 从类型 T 中选择出属性 K,构造成一个新的类型。
  • Omit<T, K>: 从类型 T 中排除掉属性 K,构造成一个新的类型。
  • Exclude<T, U>: 从 T 中排除掉那些可以赋值给 U 的类型。
  • Extract<T, U>: 从 T 中提取那些可以赋值给 U 的类型。
  • NonNullable<T>: 从 T 中排除 nullundefined
  • Parameters<T>: 获取函数类型的参数类型组成的元组类型。
  • ReturnType<T>: 获取函数类型的返回值类型。
  • InstanceType<T>: 获取构造函数类型的实例类型。
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, 'title' | 'completed'>; // { title: string; completed: boolean; }

type TodoInfo = Omit<Todo, 'description'>; // { title: string; completed: boolean; }

type Status = 'pending' | 'fulfilled' | 'rejected';
type SyncStatus = Exclude<Status, 'pending'>; // 'fulfilled' | 'rejected'

function f1(s: string) { return { a: 1, b: s }; }
type F1Params = Parameters<typeof f1>; // [s: string]
type F1ReturnType = ReturnType<typeof f1>; // { a: number; b: string; }

Swift UI数据存储

作者 Haha_bj
2025年11月26日 11:34
一. @StateObject 数据存储机制 @StateObject 保存的数据存储在设备内存(RAM)中,是临时存储 数据生命周期 创建时机:视图第一次被创建时 保持时机:视图重新渲染时数据保持不

这 5 个冷门的 HTML 标签,能让你少写 100 行 JS

作者 ErpanOmer
2025年11月25日 15:38

image.png

大家好!😁。

Code Review 的时候,我最怕看到什么?

不是复杂的算法,也不是什么正则。而是明明一个 HTML 标签就能搞定的事,有人非要写几百行 JS + CSS 去重新发明轮子

前几天,我看到一个新同学为了写一个折叠面板(Accordion),引入了一个重型的第三方库,还写了一堆 useStateonClick 和动画逻辑。

我默默地把他的代码全删了,换成了 3 行 <details>。他看我的眼神,仿佛在看一个外星人🤣。

在 2025 年的今天,浏览器原生 HTML 的能力早已今非昔比。很多我们习惯用 JS 去模拟的交互,现在不仅有原生支持,而且性能更好、兼容性更强、无障碍(a11y)更完善

今天,我就来盘点 5 个被严重低估的HTML标签👇。


<details> & <summary>:折叠组件

你是不是还在写这样的 React 代码?

// JS 模拟版
const [isOpen, setIsOpen] = useState(false);
return (
  <div className="accordion">
    <div className="header" onClick={() => setIsOpen(!isOpen)}>
      点击展开 {isOpen ? '⬆️' : '⬇️'}
    </div>
    {isOpen && <div className="content">...</div>}
  </div>
);

为了这个功能,你还得写 CSS 动画,还得处理键盘事件(Tab 键能不能选到?回车能不能展开?等等)。

HTML 原生写法:

<details>
  <summary>点击展开</summary>
  <div class="content">
    这里是展开后的内容,原生支持 Ctrl+F 页内搜索!
  </div>
</details>
  1. 没有任何JS:自带点击展开/收起交互。
  2. 无障碍(a11y)满分:屏幕阅读器能完美识别,Tab 键、回车键原生支持。
  3. 页内搜索:这是 JS 模拟版最大的痛点。如果内容被 JS 隐藏了(display: none),浏览器的 Ctrl+F 往往搜不到。但 <details> 里的内容,即使折叠,浏览器也能搜到并自动展开!

Untitled ‑ Made with FlexClip.gif

配合 CSS 👇

details {
  border: 1px solid #ccc;
  border-radius: 6px;
  padding: 8px;
}

summary {
  cursor: pointer;
  font-weight: bold;
}

/* 包住内容,让它能动画高度 */
details > .content {
  overflow: hidden;
  max-height: 0;
  opacity: 0;
  transition: max-height .45s ease, opacity .3s ease;
}

/* details 处于 open 状态时 */
details[open] > .content {
  max-height: 200px; /* 你内容高度大概多少设多少,足够大即可 */
  opacity: 1;
}

依然可以做动画。


<dialog>:弹窗组件

写模态框(Modal)是前端最大的坑之一。你需要考虑:

  • z-index 层级会不会被遮挡?
  • 点击遮罩层关闭?
  • Focus Trap(焦点锁定) :打开弹窗后,Tab 键不能跑到底层页面去。
  • 按下 Esc 键关闭?

为了解决这些,我们通常会引入 Antd Modal 或者 React Portal。但在轻量级场景下,原生 <dialog> 才是神🫅。

HTML 原生:

<dialog id="myModal">
  <form method="dialog">
    <p>这是一个原生模态框</p>
    <button>关闭(自动)</button>
  </form>
</dialog>

<button onclick="myModal.showModal()">打开弹窗⏏</button>

Untitled ‑ Made with FlexClip.gif

  1. Top Layer(顶层特性) :浏览器会把它渲染在所有 DOM 的最上层,彻底无视父元素的 z-indexoverflow: hidden
  2. ::backdrop 伪元素:直接用 CSS 定制遮罩层样式。
/* 背景遮罩 */
dialog::backdrop {
    background: rgba(0, 0, 0, 0.45);
    backdrop-filter: blur(3px);
    transition: opacity .3s ease;
}
  1. 原生交互:自带 Esc 关闭,自带焦点管理,表单提交自动关闭。

<datalist>:搜索自动补全

当产品经理要求做一个带搜索建议的输入框时,你的第一反应是不是:“快!引入 Select2 或者 Antd AutoComplete!😖

且慢。如果只是简单的建议列表,几 KB 的 JS 库都显得太重了。

HTML 原生版:

<label>选择你喜欢的框架:</label>
<input list="frameworks" />

<datalist id="frameworks">
  <option value="React">
  <option value="Vue">
  <option value="Svelte">
  <option value="Angular">
  <option value="Solid">
</datalist>
  1. 模糊搜索:浏览器原生支持模糊匹配(打 u 会出来 Vue)。
  2. 响应式:在手机上,它会调用系统原生的下拉选择 UI,体验比网页模拟的更顺滑。
  3. 解耦:它只是一个建议列表,用户依然可以输入列表里没有的值(这点和 Select 不同)。

<fieldset> & disabled:一键禁用整个表单

场景:用户点击提交按钮后,为了防止重复提交,我们需要禁用表单里的所有输入框

JS 笨办法:

// 还要一个个去拿 DOM,或者维护一个 loading 状态传给所有组件
inputs.forEach(input => input.disabled = true);
buttons.forEach(btn => btn.disabled = true);

HTML 原生写法:

<form>
  <fieldset disabled id="login-group">
    <legend>登录</legend>
    <input type="text" placeholder="用户名">
    <input type="password" placeholder="密码">
    <button>提交</button>
  </fieldset>
</form>

<script>
  // 一行代码搞定状态切换
  document.getElementById('login-group').disabled = true; 
</script>

clideo_editor_c3a7f45a392f482ea0added4098a5be3.gif

这是一个极好的分组思维。通过给 <fieldset> 设置 disabled,浏览器会自动禁用内部所有的 <input>, <select>, <button>。不用写循环,不用维护复杂的 State。


<input type="file" capture>:H5 调用手机相机

场景:业务需要用户上传一张照片,可以是相册选的,也可以是当场拍的

很多新手的反应是:是不是要接微信 JSSDK?是不是要写个 Bridge 调原生 App 能力?

答案是不需要!

HTML 原生:

<input type="file" capture="environment" accept="image/*">

只要加上 capture 属性,在移动端(iOS/Android)点击上传时,系统会直接拉起相机,而不是让你去选文件。拍完照后,你拿到的就是一个标准的 File 对象。

不需要什么 JS SDK,实现原生级体验👍。


我总是强调 最好的代码,是没有代码!

HTML 标准一直在进化,很多曾经需要重型 JS 才能实现的功能,现在已经成了浏览器的出厂设置了。

使用这些原生标签,不仅能减少打包体积,更能让你的应用在可访问性性能上天然领先。

下次再想 npm install 一个 UI 库之前,先查查 MDN。说不定,HTML 早就帮你做好了🤔。

❌
❌