普通视图

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

WKWebView的重定向(objective_c)

2025年11月29日 18:12

背景

第三方支付回调时需要重定向到app的某个页面,比如支付完成后回到原生订单详情页,这个时间会有两种情况:

1、直接在web页面重定向到app的订单详情页,这个时候只需要实现 WKNavigationDelegate 中的一个核心方法webView:decidePolicyForNavigationAction:decisionHandler: 方法。

2、在支付中心跳转到第三方app然后支付完成后需要跳转回自己的app的订单详情页,这个时候可以采用Scheme方式或者是通用链接的方式解决

wkWebView重定向实现

实现这一目标,您需要让您的 WKWebView 所在的控制器遵循 WKNavigationDelegate 协议,并实现 webView:decidePolicyForNavigationAction:decisionHandler: 方法。

self.webView.navigationDelegate = self; // 设置代理

#pragma mark - WKNavigationDelegate 
- (**void**)webView:(WKWebView *)webView

decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction

decisionHandler:(**void** (^)(WKNavigationActionPolicy))decisionHandler {
    NSURL *url = navigationAction.request.URL;
    NSString *scheme = url.scheme;
    // 1. 检查 URL Scheme 是否是我们的自定义 Scheme
    if ([scheme isEqualToString:@"coolpet"]) {
        // 1.1. 阻止 WKWebView 加载这个 URL
        decisionHandler(WKNavigationActionPolicyCancel);
        // 1.2. 实现了 handleCoolPetURL: 方法
        [self handleCoolPetURL:url];
        // 1.3. 跳转后关闭当前的 WebView 页面
        [self.navigationController popViewControllerAnimated:YES];
        return;
    }
    // 2. 对于其他 HTTP/HTTPS 链接,允许正常加载
    // 特别检查 navigationType 是否是新的主框架加载,例如用户点击了链接
//    if (navigationAction.navigationType == WKNavigationTypeLinkActivated && ![scheme hasPrefix:@"http"]) {
//        // 如果是点击了非 HTTP/HTTPS 的链接(但不是我们自定义的 Scheme),可以根据需要处理,
//        // 比如打开 App Store 或其他应用。这里我们通常允许其他系统 Scheme
//        // 允许继续,但更安全的做法是只允许 http(s)
//        // decisionHandler(WKNavigationActionPolicyAllow);
//    }
    // 3. 默认允许其他所有导航行为(如页内跳转、HTTP/HTTPS 加载等)
    decisionHandler(WKNavigationActionPolicyAllow);
}

// 通过URL跳转对应页面
- (void)handleCoolPetURL:(NSURL *)url {
    NSString *host = url.host;
    NSString *path = url.path;      // 路径: /order/detail
    NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
    NSMutableDictionary *queryParams = [NSMutableDictionary dictionary];
    for (NSURLQueryItem *item in components.queryItems) {
        queryParams[item.name] = item.value;
    }
    // 根据路径判断是否是订单详情页
    if ([host isEqualToString:kAPPUniversalTypeOrderDetailsHost] && [path isEqualToString:kAPPUniversalTypeOrderDetailsPath]) {
        // 获取我们需要的订单号
        NSString *tradeNo = [queryParams[@"tradeNo"] stringValue];
        // 执行跳转

        if (tradeNo.length > 0) {
            dispatch_async(dispatch_get_main_queue(), ^{
                /// 做跳转
            });
        }
    }
}

Scheme方式

第三方支付平台完成支付后,是通过你App的 URL Scheme 来唤醒你的App并携带支付结果的。

  1. 配置 App URL Scheme
  • 操作: 在 Xcode 项目的 Info.plist 或项目设置的 Info 选项卡下的 URL Types 中添加你的 App 的 Scheme。

    • 例如,你可以设置一个 Scheme 叫 myscheme
  1. 处理 App Delegate 中的回调

App 被第三方支付应用唤醒后,系统会调用 AppDelegate 中的特定方法。你需要在这里接收并处理回调 URL。

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, **id**> *)options {
    // 1. 检查是否是你的支付回调 Scheme
    if ([url.scheme isEqualToString:@"myappscheme"]) {
        [self handleCoolPetURL:url];
    }

    // 如果是其他URL(如通用链接),也在这里处理
    // ...
    return NO;
}

通用链接方式

当用户点击一个配置了通用链接的 HTTPS 链接时:

  1. 如果 App 已经安装,系统会直接调用 AppDelegate 中的这个方法。
  2. 如果 App 未安装,该链接会直接在 Safari 中打开。

这个机制的主要优点是安全(基于 HTTPS)和用户体验更好(避免了 URL Scheme 引起的跳转确认和安全问题)。

🔗 通用链接(Universal Links)实现指南

步骤 1: 服务器端配置(Association File)

这是通用链接能够工作的基础。您需要在您的 Web 服务器上创建一个特殊的 JSON 文件,告诉 iOS 系统哪些路径应该由您的 App 处理。

1. 创建 apple-app-site-association 文件
  • 文件名: 必须是 apple-app-site-association(注意,没有 .json 扩展名)。

  • 内容格式(JSON):

    {
        "applinks": {
            "apps": [],
            "details": [
                {
                    "appID": "TeamID.BundleID",
                    "paths": [
                        "/orders/*",    // 匹配所有 /orders/ 下的路径
                        "/products/*",  // 匹配所有 /products/ 下的路径
                        "NOT /account/login/*" // 排除某些路径
                    ]
                }
            ]
        }
    }
    
    • TeamID 您的 Apple Developer Team ID。
    • BundleID 您的 App 的 Bundle Identifier。
    • paths 定义您希望 App 能够处理的 URL 路径。
2. 部署文件
  • 部署位置: 将此文件上传到您的域名根目录或 .well-known/ 目录下。

    • 例如:https://yourdomain.com/apple-app-site-association
    • 或者:https://yourdomain.com/.well-known/apple-app-site-association
  • 内容类型: 确保服务器以正确的 MIME 类型提供此文件:application/jsontext/plain

  • HTTPS: 您的整个网站必须使用 HTTPS

步骤 2: App 端配置(Xcode & Objective-C)

1. 开启 Associated Domains Capability

在 Xcode 中为您的 App 开启 Associated Domains 功能。

  • 路径: Xcode -> 项目设置 -> 目标 (Target) -> Signing & Capabilities 选项卡

  • 操作: 点击 + Capability,添加 Associated Domains

  • 添加域名: 在列表中添加您的域名,格式为:

    applinks:yourdomain.com
    

    注意: 不带 https://http://

2. 在 AppDelegate 中接收回调

当用户点击一个通用链接并唤醒 App 时,系统会调用 AppDelegate 中的 continueUserActivity 方法。您需要在此方法中解析 URL 并进行页面跳转。

// AppDelegate.m

#import "OrderViewController.h" // 假设您的订单处理页面

// ...

- (BOOL)application:(UIApplication *)application 
continueUserActivity:(NSUserActivity *)userActivity 
  restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
    
    // 1. 检查活动类型是否为 Universal Link
    if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
        
        // 2. 获取用户点击的 HTTPS URL
        NSURL *webpageURL = userActivity.webpageURL;
        
        if (webpageURL) {
            NSLog(@"Received Universal Link: %@", webpageURL.absoluteString);
            
            // 3. 将 URL 转发给路由处理方法
            [self handleUniversalLinkURL:webpageURL];
            
            return YES;
        }
    }
    
    return NO;
}

// 通用链接路由处理方法
- (void)handleUniversalLinkURL:(NSURL *)url {
    
    // 示例:解析路径并跳转到订单详情
    if ([url.path hasPrefix:@"/orders/detail"]) {
        
        // 解析查询参数,例如 order_id=12345
        NSString *orderID = [self extractParameter:@"order_id" fromURL:url];
        
        if (orderID.length > 0) {
            dispatch_async(dispatch_get_main_queue(), ^{
                // 执行跳转逻辑
                UINavigationController *nav = (UINavigationController *)self.window.rootViewController;
                OrderViewController *orderVC = [[OrderViewController alloc] init];
                orderVC.orderID = orderID;
                [nav pushViewController:orderVC animated:YES];
            });
        }
    }
}

// 辅助方法 (需要您自行实现,或使用前文提到的 dictionaryWithQueryString: 方法)
- (NSString *)extractParameter:(NSString *)paramName fromURL:(NSURL *)url {
    // ... 解析 url.query 字符串,提取指定参数 ...
    return nil; 
}

华为Mate X7 云锦天章:高级感,从何而来?

作者 谢东成
2025年11月29日 18:05

《红楼梦》中最盛大的一幕,莫过于元春省亲——「锦幢云幕,珠履罗衢」。

「翻阅《红楼梦》,满目皆云锦。」曹雪芹并不靠堆砌珠宝来描绘华贵,而是以织物来传达秩序之美:锦帐、妆花、帷幕层层递进,礼与雅由经纬而生。也正因如此,谈云锦,似乎天然就带着一份制度与审美同构的分寸感。

如今,华为Mate X7 的「光织云锦」背板,正是将这门古老工艺从礼制器物转译为当代材料语言的大胆尝试——让云锦在今天的工业体系中焕发新辉,把文化的经纬,织入日用之物的触感与光影。

由经纬入骨,由材质成形

早于公元 417 年,东晋于建康设立「锦署」,自此南京云锦与权力与礼制相系相生。及至元、明、清三代,云锦均为皇室御用品,龙袍、冕服皆出其间,成为礼制文明最华美的注脚。

云锦之珍稀,不独在纹样之繁复,亦在工具与手艺之精微。

「大花楼」织机通高四米:楼上拽花提经,楼下抛梭织纬,二人合拍,一经一纬,一日仅得五六厘米。

▲ 位于南京云锦博物馆的大花楼木织机,图片来自:金羊网

云锦之所以叫云锦,正因为它「灿若云霞」,其「妆花」是云锦中艺术成就最高的品类,其要诀在于「挖花盘织」「逐花异色」:在同一幅布面上,以丝线、金线、银线、孔雀羽等异材并用、分区换色,分别织出各异纹样。

换个角度看,光影会变,层次会变,仿佛织物在呼吸,生机自现。

在嘉庆年间,南京云锦织造达到顶峰,全城织机三万多台,二十多万人以此为业,秦淮河一带机户云集,机杼声彻夜不绝。只可惜,清末以降行当凋敝,织造手艺一度失传。至 1949 年南京仅存可生产织机四台。1980 年,南京云锦研究所承接北京定陵博物馆龙袍料复织之任,由复原入手,重新体悟与还原这门技艺的工序与精义。

终于在 2009 年,云锦织造技艺入列联合国人类非物质文化遗产代表作名录,云锦技艺谱系由此再度庄严确立,并得以传承。

▲ 云锦复原款,图片来自:南京云锦博物馆

若要将「云锦」这套关于经纬、叠层、秩序的复杂逻辑,与现代智能手机相结合,就必须从材料逻辑而非图案贴附入手。

正如华为Mate X7 所带来的「光织云锦」背板,就是把云锦的织造逻辑,用现代材料科学重新实现一遍。

传统云锦以无数根经纬织就纹理,经线为纵向骨架,纬线为横向填充,经纬相交、密度有别、材质各异,便生出不同的纹理与光泽。

基于这套方法论,华为Mate X7 则在背板中以纳米级纤维重现这套「经/纬」的排布逻辑。通过约 900 根经线 × 1700 根纬线的精密交织,呈现祥云纹理,让纤维具备明确的方向性与密度差,在不同视线角度下,会呈现出层次各异的光影变化。

与此同时,华为Mate X7 亦从云锦「孔雀羽」以结构色呈现鲜艳反光汲取灵感,将纳米级多层光泽膜切丝入捻,融入纤维体系。

随光而动,呈现细腻而鲜明的色彩流变——一处是丝绸的柔哑,一处是金属的冷光,间或隐现孔雀羽的幽蓝,一面之上自成多层光影。

远看是层层叠叠的金属流光,近看是细密纤维,如「金丝」勾勒星河,云纹交织万象,营造出「手可摘星辰」的诗意与浪漫质感,观感与触感彼此叠映,既克制雅致,又丰盈饱满。

▲ 华为Mate X7 云锦白、云锦蓝配色,图片来自:燕山派

华为Mate X7 把这种经纬逻辑固化到工业产线,让云锦质感得以复现,带来云锦白、云锦蓝两款极具东方美学意境的配色,并将轻量化、高强度与卓越抗冲击性巧妙融合,在确保观感与手感的前提下,也不牺牲日常所需的耐用与防护。

承托这块云锦背板的,是全新超可靠折叠玄武架构机身,外有第二代玄武钢化昆仑玻璃外屏,内有超韧三重复合叠层内屏,中间由玄武水滴铰链和超强机翼铝中框共同支撑。

这套架构的存在,让华为Mate X7 用起来更踏实,反复开合不心疼,且华为Mate X7 达到 IP58 和 IP59 防尘抗水的顶级防护水准,80℃ 热水喷淋也不犯怵。

如此一来,手机不再是把非遗技艺做成表层贴花,而是让云锦织造逻辑内嵌于材料与结构,成为既有层次与意蕴、也经得起日常使用的掌心器物。

一瞥惊鸿的「时空之门」

门,是人类为世界划出的第一道分界:门内是秩序与日常,门外是未知与远方。

折叠屏手机的每一次开合,也像是在一扇门之间来回穿梭:在灵动外屏和开阔内屏之间,在速览与沉浸之间,也在掏出记录与铺开创作之间。

信息与视野随之跃迁,叙事与效率也在开合之间里完成切换。

华为Mate X7 将这层意象做成了看得见的标志性符号,收归于名为「时空之门」的一体化镜头模组之中。

这道时空之门延续了 Mate X 折叠屏系列从寰宇舷窗、寰宇星门、寰宇星轨演变而来的寰宇系列设计语言。

立体多维的「时空之门」模组,被安排在机身中轴偏上的位置,四周通过不同材质和颜色的过渡做出一种类似「门框」的效果。

边缘有切面、有高光、有层级感,给华为Mate X7 定下一个过目难忘的视觉重心。

在视觉上,它和云锦/素皮背板形成对话。一个提供温润的、流动的、有呼吸感的背景;一个提供理性的、几何的、稳定的视觉锚点。

两者结合,让整台机器在折叠和展开时,都有清晰的设计逻辑可循。

当然,镜头模组本身,就是通往光影世界的入口,也要为了内在影像表达的功能性而服务。

华为Mate X7 搭载了「第二代红枫原色摄像头」,其「光谱感知」能力得到跨越升级,色彩分辨力提升 25%、进光量提升 96%、色彩还原能力提升 43%,在复杂的混光场景也绝不偏色。

更重要的是,主摄采用了首创四切片镜片设计,配合 0.4mm 业界超薄玻璃镜片,既提升了进光量,又缩小了模组体积,本是相互冲突的因素,在华为Mate X7 身上,得到了妥善平衡的答案。

典藏版搭载折叠屏业界最大底——1/1.28 英寸 RYYB 超光变传感器,拥有 5000 万高像素,配备 F1.49 – F4.0 十档可变光圈,超强感光,无论明暗准出片。

配备的业界最大光圈潜望式长焦镜头,则通过分群对焦镜片组与长焦微距直立潜望系统,融合多维一体化对焦防抖马达,在极限体积里实现了光学规格的跃升。

这意味着,华为Mate X7 把堪比直板旗舰的超高规格影像,成功地塞进了极致轻薄的折叠机身之中,不再需要为了轻薄和折叠结构,而作出任何妥协。

真正高端的折叠屏手机,如何表达「高级感」设计?

历经六年迭代,折叠屏手机早已迈过了从尝鲜到常用的临界点。而在这个细分品类中,华为长期稳居份额第一,已成为公认的「折叠标杆」。

在这样的基础上,华为Mate X7 再度以「集大成」的姿态,给出更完整、更立体的答案:把看得见的设计、摸得着的质感、用得住的可靠、拍得好的影像、离不开的智能,串成一整段连贯的使用体验。

它以超可靠玄武折叠架构、耐用铰链与高等级防护把「轻薄」与「耐用」同时落地;再以第二代红枫影像与更强的主摄/长焦协同,让复杂光线与远近场景都能稳定出片;而在大屏场景与 AI 体验上,鸿蒙 6 大屏 AI、分屏协作与跨设备互联,让折叠形态从「好看」走向「高效」。

在解决轻薄、耐用、性能、影像等一个个棘手问题之后,高端旗舰手机又开始回到那个基础的命题:

到底应该用什么,来定义产品的「高级感」?

这并非突发的转向,而是长期积累后的必然。

随着中国厂商在供应链、工艺与设计上的成熟与自信,他们既有能力、也有必要回答更深的问题:中国高端手机到底应该长什么样?它的质感由何承载?用什么材料与表达去呈现?

作为折叠屏手机的佼佼者,华为Mate X7 的回答,是将云锦这种承载了 1600 年历史的非遗工艺,用现代工业的方式,在一块手机背板上重新演绎。

这并非简单粗暴地把纹样贴上去,而是选择先读懂云锦的核心:

经纬如何组织秩序,多材质叠层如何生成光影,逐花异色如何堆叠细节;随后再用现代材料科学,把这套逻辑重建为纳米纤维的经纬与工艺叠层,让纹理从材质里「长」出来。

这样的尝试,在中国消费电子史上并不鲜见,失败的例子也很多:仿陶瓷、仿织造、仿书画,最后只剩「形似神离」。

但华为Mate X7 的回答,不是模仿表面,而是将文化内核写进材料与结构本身。

放眼长远,华为Mate X7 站在中国高端手机与折叠形态演进的时间轴上,是一个值得标注的坐标:它昭示着中国高端智能手机正在以非遗的材料语言,给出关于「高级感」的答案。

夜里关灯前,你合上手机,随手搁在床头。微光掠过背板,那层纳米纤维经纬轻轻起伏;你未必了解「大花楼」与纳米工艺,但在每天打开它的那一刻,触摸到与玻璃、塑料不一样的指尖质感。

这份细微差异,无法明确写进参数表,却会慢慢渗入日常。

几百年前,云锦是权力与礼制的象征;几百年后,它亦化作华为Mate X7 的机身外壳,合于掌心阅信,展为大屏协作,立在桌角静赏光影——从宫廷器物到日用之物,文化的经与纬,仍在掌心流转。

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

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


mac电脑安装nvm

2025年11月29日 18:03

方案一、常规安装

  1. 下载安装脚本:在终端中执行以下命令来下载并运行 NVM 的安装脚本3:

    bash

    curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.39.5/install.sh | bash
    

  2. 配置环境变量:安装完成后,需要配置环境变量。如果你的终端使用的是 bash,打开或创建~/.bash_profile文件,添加以下内容3:

    bash

    export NVM_DIR="$HOME/.nvm"
    [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # 加载nvm
    [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # 加载bash自动补全(可选)
    

    如果使用的是 zsh,则打开或创建~/.zshrc文件,添加相同内容。然后执行source ~/.bash_profilesource ~/.zshrc使配置生效。

方案二、解决网络问题的安装

如果因为网络原因无法直接访问官方源,可以尝试以下方法:

  1. 通过国内镜像下载安装脚本:可以从 gitee 等国内代码托管平台的镜像下载安装脚本,例如:

    bash

    curl -o- https://gitee.com/cunkai/nvm-cn/raw/master/install.sh | bash
    

  2. 配置 NVM 使用国内镜像:安装完成后,编辑~/.zshrc(或~/.bashrc),添加以下内容来配置 NVM 使用国内的 Node.js 镜像源:

    bash

    export NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node
    export NVM_IOJS_ORG_MIRROR=https://npmmirror.com/mirrors/iojs
    

    保存后执行source ~/.zshrcsource ~/.bashrc使配置生效。

安装完成后,可以通过nvm -v命令查看 NVM 的版本,以确认是否安装成功。

nvm常用命令

;安装node18.16.0
nvm install 18.16.0

;查看nvm安装的node版本
nvm list

;通过nvm list查看电脑已有的版本号,设置默认的版本
nvm alias default v22.16.0

iOS 语音房(拍卖房)开发实践

作者 KangJX
2025年11月29日 17:49

本文基于一个真实的iOS语音房项目案例,详细讲解如何使用状态模式来管理复杂的业务流程,以及如何与权限中心协同工作,因为在拍卖房间中不只有不同的房间阶段变化(状态)还有不同角色拥有不同的权限(权限中心)。

业务场景

拍拍房是一个实时拍卖房间系统,类似于语音房间+拍卖的结合体。用户可以在房间内:

  • 作为房主主持拍卖
  • 作为拍卖人上传物品并介绍
  • 作为竞拍者出价竞拍
  • 作为观众观看拍卖过程

核心业务流程

一个完整的拍卖流程需要经历4个明确的阶段:

准备阶段 → 上拍 → 拍卖中 → 定拍 → (循环)准备阶段

每个阶段都有:

  • 不同的允许操作(如只能在准备阶段上传物品)
  • 不同的状态转换规则(如只能从拍卖中进入定拍)
  • 不同的业务逻辑(如只有拍卖中才能出价)

技术挑战

  1. 状态多:4个主要状态,每个状态行为差异大
  2. 转换复杂:状态之间的转换有严格的规则
  3. 权限交织:每个操作还需要考虑用户角色权限
  4. 易扩展性:未来可能增加新的拍卖模式

为什么选择状态模式

❌ 不使用状态模式的问题

如果使用传统的 if-elseswitch-case 来处理:

// 反例:所有逻辑堆砌在一起
func placeBid(amount: Decimal) {
    if currentState == .preparing {
        print("拍卖还未开始")
        return
    } else if currentState == .listing {
        print("拍卖还未正式开始")
        return
    } else if currentState == .auctioning {
        // 执行出价逻辑
        if user.role == .viewer {
            print("观众不能出价")
            return
        }
        if user.id == auctioneer.id {
            print("拍卖人不能给自己出价")
            return
        }
        if amount < currentPrice + incrementStep {
            print("出价金额不足")
            return
        }
        // 终于可以出价了...
    } else if currentState == .closed {
        print("拍卖已结束")
        return
    }
}

问题显而易见

  1. 🔴 代码臃肿:所有状态的逻辑混在一起
  2. 🔴 难以维护:修改一个状态可能影响其他状态
  3. 🔴 不易扩展:增加新状态需要修改多处代码
  4. 🔴 权限混乱:业务逻辑和权限判断纠缠在一起
  5. 🔴 测试困难:无法单独测试某个状态的逻辑

✅ 使用状态模式的优势

// 状态模式:每个状态独立处理
class AuctioningState: RoomStateProtocol {
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        // 只关注拍卖中状态的出价逻辑
        let bid = Bid(...)
        room.addBid(bid)
        return true
    }
}

class PreparingState: RoomStateProtocol {
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        // 准备阶段直接拒绝
        print("拍卖还未开始")
        return false
    }
}

优势明显

  1. 职责单一:每个状态类只关注自己的逻辑
  2. 易于维护:修改某个状态不影响其他状态
  3. 开闭原则:新增状态只需添加新类,不修改现有代码
  4. 清晰直观:状态转换一目了然
  5. 便于测试:可以单独测试每个状态

状态模式设计

整体架构

┌─────────────────────────────────────────┐
│           Room(房间上下文)             │
│  - currentState: RoomStateProtocol      │
│  - changeState(to: RoomState)           │
└──────────────┬──────────────────────────┘
               │ 持有
               ↓
┌─────────────────────────────────────────┐
│      RoomStateProtocol(状态协议)       │
│  + startAuction(room: Room) -> Bool     │
│  + placeBid(room: Room, ...) -> Bool    │
│  + endAuction(room: Room) -> Bool       │
│  + uploadItem(room: Room, ...) -> Bool  │
└─────────────┬───────────────────────────┘
              │ 实现
    ┌─────────┼─────────┬─────────┐
    ↓         ↓         ↓         ↓
┌──────┐ ┌────────┐ ┌────────┐ ┌────────┐
│准备  │ │上拍    │ │拍卖中  │ │定拍    │
│State │ │State   │ │State   │ │State   │
└──────┘ └────────┘ └────────┘ └────────┘

核心组件

1. 状态枚举

enum RoomState: String {
    case preparing      // 准备阶段
    case listing        // 上拍
    case auctioning     // 拍卖中
    case closed         // 定拍
}

2. 状态协议

protocol RoomStateProtocol {
    var stateName: RoomState { get }
    
    // 状态转换
    func startAuction(room: Room) -> Bool
    func endAuction(room: Room) -> Bool
    
    // 业务操作
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool
    func uploadItem(room: Room, item: AuctionItem, rules: AuctionRules) -> Bool
    
    // 状态描述
    func getStateDescription() -> String
}

状态转换图

┌─────────────┐
│  准备阶段    │ 拍卖人上传物品、设置规则
│  Preparing  │ 房主可以开始拍卖
└──────┬──────┘
       │ startAuction()
       ↓
┌─────────────┐
│    上拍     │ 展示物品信息
│   Listing   │ 倒计时准备(3秒)
└──────┬──────┘
       │ 自动转换 / 房主提前开始
       ↓
┌─────────────┐
│   拍卖中    │ 用户可以出价
│ Auctioning  │ 倒计时重置机制
└──────┬──────┘
       │ endAuction() / 倒计时归零
       ↓
┌─────────────┐
│    定拍     │ 展示成交结果
│   Closed    │ 可以开启下一轮
└──────┬──────┘
       │ startAuction() (开启下一轮)
       ↓
┌─────────────┐
│  准备阶段    │ 回到初始状态
│  Preparing  │
└─────────────┘

具体实现

1. 准备阶段(Preparing)

class PreparingState: RoomStateProtocol {
    var stateName: RoomState { return .preparing }
    
    // ✅ 允许:开始拍卖
    func startAuction(room: Room) -> Bool {
        guard room.currentItem != nil else {
            print("⚠️ 没有拍卖物品,无法开始")
            return false
        }
        
        // 状态转换:准备 → 上拍
        room.changeState(to: .listing)
        
        // 3秒后自动进入拍卖中
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            room.changeState(to: .auctioning)
        }
        
        return true
    }
    
    // ❌ 不允许:出价
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        print("⚠️ 拍卖还未开始,无法出价")
        return false
    }
    
    // ✅ 允许:上传物品
    func uploadItem(room: Room, item: AuctionItem, rules: AuctionRules) -> Bool {
        room.setAuctionItem(item, rules: rules)
        return true
    }
    
    // ❌ 不允许:结束拍卖
    func endAuction(room: Room) -> Bool {
        print("⚠️ 拍卖还未开始")
        return false
    }
    
    func getStateDescription() -> String {
        return "准备阶段:拍卖人可以上传物品并设置规则"
    }
}

关键点

  • ✅ 只允许上传物品和开始拍卖
  • ✅ 自动触发状态转换(准备 → 上拍 → 拍卖中)
  • ✅ 逻辑清晰,职责单一

2. 上拍阶段(Listing)

class ListingState: RoomStateProtocol {
    var stateName: RoomState { return .listing }
    
    // ✅ 允许:房主提前开始
    func startAuction(room: Room) -> Bool {
        room.changeState(to: .auctioning)
        return true
    }
    
    // ❌ 不允许:出价
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        print("⚠️ 拍卖还未正式开始,无法出价")
        return false
    }
    
    // ❌ 不允许:修改物品
    func uploadItem(room: Room, item: AuctionItem, rules: AuctionRules) -> Bool {
        print("⚠️ 上拍阶段无法修改物品")
        return false
    }
    
    // ❌ 不允许:结束拍卖
    func endAuction(room: Room) -> Bool {
        print("⚠️ 拍卖还未正式开始")
        return false
    }
    
    func getStateDescription() -> String {
        return "上拍中:展示拍卖物品,倒计时后自动开始"
    }
}

关键点

  • 🎯 过渡状态:用于展示物品信息
  • ✅ 房主可以提前开始
  • ❌ 大部分操作被禁止,保证流程的严谨性

3. 拍卖中(Auctioning)⭐ 核心状态

class AuctioningState: RoomStateProtocol {
    var stateName: RoomState { return .auctioning }
    
    // ❌ 不允许:重复开始
    func startAuction(room: Room) -> Bool {
        print("⚠️ 拍卖已经在进行中")
        return false
    }
    
    // ✅ 允许:结束拍卖
    func endAuction(room: Room) -> Bool {
        room.changeState(to: .closed)
        
        if let winner = room.currentBid {
            room.addSystemMessage("🎉 成交!恭喜 (winner.bidderName) 以 ¥(winner.price) 拍得")
        } else {
            room.addSystemMessage("流拍:没有人出价")
        }
        
        return true
    }
    
    // ✅ 允许:出价(核心逻辑)
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        // 创建出价记录
        let bid = Bid(
            id: UUID().uuidString,
            price: amount,
            bidderId: user.id,
            bidderName: user.nickname,
            timestamp: Date()
        )
        
        // 记录出价
        room.addBid(bid)
        
        print("💰 (user.nickname) 出价 ¥(amount)")
        
        // 这里可以重置倒计时(简化版省略)
        // resetCountdown()
        
        return true
    }
    
    // ❌ 不允许:修改物品
    func uploadItem(room: Room, item: AuctionItem, rules: AuctionRules) -> Bool {
        print("⚠️ 拍卖进行中,无法修改物品")
        return false
    }
    
    func getStateDescription() -> String {
        return "拍卖中:竞拍者可以出价,倒计时结束后定拍"
    }
}

关键点

  • 💰 核心业务逻辑:处理出价
  • 📊 实时更新:记录每次出价
  • ⏱️ 倒计时机制:有出价时重置(可扩展)
  • 🔄 状态转换:可以结束进入定拍

4. 定拍阶段(Closed)

class ClosedState: RoomStateProtocol {
    var stateName: RoomState { return .closed }
    
    // ✅ 允许:开启下一轮
    func startAuction(room: Room) -> Bool {
        // 重置房间状态
        room.changeState(to: .preparing)
        room.currentItem = nil
        room.currentBid = nil
        room.addSystemMessage("🔄 准备下一轮拍卖")
        return true
    }
    
    // ❌ 不允许:出价
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        print("⚠️ 拍卖已经结束,无法出价")
        return false
    }
    
    // ❌ 不允许:重复结束
    func endAuction(room: Room) -> Bool {
        print("⚠️ 拍卖已经结束")
        return false
    }
    
    // ❌ 不允许:上传物品
    func uploadItem(room: Room, item: AuctionItem, rules: AuctionRules) -> Bool {
        print("⚠️ 拍卖已结束,请开启下一轮")
        return false
    }
    
    func getStateDescription() -> String {
        return "已定拍:拍卖结束,可以开启下一轮"
    }
}

关键点

  • 🎉 展示成交结果
  • 🔄 支持循环拍卖:可以开启下一轮
  • 🔒 所有拍卖操作被锁定

与权限中心协作

设计哲学:分离关注点

┌─────────────────────────────────────┐
│         用户发起操作                 │
└──────────────┬──────────────────────┘
               ↓
┌─────────────────────────────────────┐
│      RoomManager(协调层)           │
└──────────────┬──────────────────────┘
               ↓
        ┌──────┴──────┐
        ↓             ↓
┌──────────────┐ ┌──────────────┐
│ 权限中心      │ │ 状态对象      │
│"能不能做"    │ │"怎么做"      │
└──────────────┘ └──────────────┘

协作流程

class RoomManager {
    func placeBid(user: User, room: Room, amount: Decimal) -> Result<Void, RoomError> {
        // 第一步:权限中心检查"能不能做"
        let result = permissionCenter.checkPermission(
            action: .placeBid,
            user: user,
            room: room,
            metadata: ["amount": amount]
        )
        
        guard result.isAllowed else {
            return .failure(.permissionDenied(result.deniedReason ?? "权限不足"))
        }
        
        // 第二步:状态对象执行"怎么做"
        let success = room.stateObject.placeBid(room: room, user: user, amount: amount)
        
        if success {
            return .success(())
        } else {
            return .failure(.operationFailed("出价失败"))
        }
    }
}

权限规则示例

// 权限中心:检查"能不能做"
PermissionRule(
    action: .placeBid,
    priority: 100,
    description: "只能在拍卖中状态出价"
) { context in
    guard context.room.state == .auctioning else {
        return .denied(reason: "❌ 当前不在拍卖阶段,无法出价")
    }
    return .allowed
}

PermissionRule(
    action: .placeBid,
    priority: 90,
    description: "拍卖人不能给自己出价"
) { context in
    if context.user.role == .auctioneer,
       context.user.id == context.room.currentItem?.auctioneerId {
        return .denied(reason: "❌ 您是拍卖人,不能对自己的物品出价")
    }
    return .allowed
}

为什么要分离?

如果不分离

// ❌ 反例:状态和权限混在一起
class AuctioningState {
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        // 权限判断
        if user.role == .viewer {
            return false
        }
        if user.role == .auctioneer && user.id == auctioneer.id {
            return false
        }
        if amount < currentPrice + increment {
            return false
        }
        
        // 业务逻辑
        room.addBid(...)
        return true
    }
}

分离后

// ✅ 状态对象:只关心业务逻辑
class AuctioningState {
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        room.addBid(...)  // 纯粹的业务逻辑
        return true
    }
}

// ✅ 权限中心:只关心权限验证
PermissionCenter.check(.placeBid, user, room)

优势

  1. 单一职责:状态对象不关心权限
  2. 易于扩展:新增权限规则不影响状态
  3. 易于测试:可以独立测试权限和状态
  4. 灵活配置:权限规则可以动态调整

实际应用场景

场景1:完整拍卖流程

// 1. 创建房间(自动进入准备阶段)
let room = Room(name: "今晚靓号专场", owner: host)
print("房间状态:(room.state.displayName)") // 准备中

// 2. 拍卖人上传物品
let item = AuctionItem(name: "手机号 13888888888", ...)
room.stateObject.uploadItem(room: room, item: item, rules: rules)
// ✅ 准备阶段允许上传物品

// 3. 房主开始拍卖
room.stateObject.startAuction(room: room)
// 状态转换:准备 → 上拍
print("房间状态:(room.state.displayName)") // 上拍中

// 4. 3秒后自动进入拍卖中
// 状态转换:上拍 → 拍卖中
print("房间状态:(room.state.displayName)") // 拍卖中

// 5. 竞拍者出价
room.stateObject.placeBid(room: room, user: bidder1, amount: 120)
// ✅ 拍卖中状态允许出价
print("当前价格:¥(room.currentPrice)") // ¥120

room.stateObject.placeBid(room: room, user: bidder2, amount: 150)
print("当前价格:¥(room.currentPrice)") // ¥150

// 6. 房主结束拍卖
room.stateObject.endAuction(room: room)
// 状态转换:拍卖中 → 定拍
print("房间状态:(room.state.displayName)") // 已定拍
print("成交:(room.currentLeader) - ¥(room.currentPrice)")

// 7. 开启下一轮
room.stateObject.startAuction(room: room)
// 状态转换:定拍 → 准备
print("房间状态:(room.state.displayName)") // 准备中

场景2:错误的操作被拒绝

// 尝试在准备阶段出价
let room = Room(...)
room.stateObject.placeBid(room: room, user: bidder, amount: 200)
// ❌ 输出:"拍卖还未开始,无法出价"
// 返回:false

// 尝试在拍卖中上传物品
room.stateObject.startAuction(room: room)  // 进入拍卖中
room.stateObject.uploadItem(room: room, item: item, rules: rules)
// ❌ 输出:"拍卖进行中,无法修改物品"
// 返回:false

// 尝试在定拍后出价
room.stateObject.endAuction(room: room)  // 进入定拍
room.stateObject.placeBid(room: room, user: bidder, amount: 300)
// ❌ 输出:"拍卖已经结束,无法出价"
// 返回:false

场景3:状态转换的严格性

let room = Room(...)

// ✅ 正确的转换
room.state  // .preparing
room.stateObject.startAuction(room: room)
room.state  // .listing → .auctioning

// ❌ 不允许跳过状态
room.state  // .preparing
room.stateObject.endAuction(room: room)
// 输出:"拍卖还未开始"
// 状态不变,仍然是 .preparing

优势与挑战

✅ 优势

1. 代码组织清晰

对比

传统方式(500行的switch):

func handleOperation() {
    switch currentState {
    case .preparing:
        // 100行代码
    case .listing:
        // 100行代码
    case .auctioning:
        // 200行代码
    case .closed:
        // 100行代码
    }
}

状态模式(每个文件<100行):

PreparingState.swift    // 80行
ListingState.swift      // 60行
AuctioningState.swift   // 100行
ClosedState.swift       // 60行

2. 易于维护

修改"拍卖中"的逻辑:

  • ❌ 传统方式:在500行代码中找到对应的case,小心翼翼地修改
  • ✅ 状态模式:直接打开AuctioningState.swift,放心修改

3. 符合开闭原则

新增"暂停"状态:

  • ❌ 传统方式:修改所有的switch语句,增加新的case
  • ✅ 状态模式:创建PausedState.swift,不修改现有代码

4. 便于测试

// 可以单独测试某个状态
func testAuctioningState() {
    let state = AuctioningState()
    let room = MockRoom()
    let result = state.placeBid(room: room, user: mockUser, amount: 100)
    XCTAssertTrue(result)
}

5. 团队协作友好

多人开发时:

  • 小明负责 PreparingState
  • 小红负责 AuctioningState
  • 小刚负责 ClosedState

互不干扰,Git冲突少。

⚠️ 挑战

1. 类的数量增加

  • 4个状态 = 4个类文件
  • 如果有10个状态,就需要10个文件

应对:合理的文件组织和命名规范

2. 状态转换的复杂性

需要仔细设计状态转换图,避免:

  • 死锁状态
  • 循环转换
  • 无法到达的状态

应对

  • 绘制状态图
  • 编写状态转换测试
  • 文档化转换规则

3. 状态间的数据共享

状态对象是无状态的,数据存储在Room对象中:

class Room {
    var stateObject: RoomStateProtocol  // 当前状态对象
    var currentItem: AuctionItem?       // 状态间共享的数据
    var currentBid: Bid?                // 状态间共享的数据
}

应对

  • 明确哪些数据属于上下文(Room)
  • 哪些数据属于状态对象

4. 调试可能更困难

调用链变长:

ViewController → RoomManager → PermissionCenter → StateObject

应对

  • 添加详细的日志
  • 使用断点调试
  • 编写单元测试

最佳实践

1. 状态对象应该是无状态的

// ❌ 错误:状态对象持有数据
class AuctioningState {
    var currentPrice: Decimal = 0  // 不应该在这里
    var bidHistory: [Bid] = []     // 不应该在这里
}

// ✅ 正确:数据存储在上下文中
class Room {
    var currentPrice: Decimal
    var bidHistory: [Bid]
    var stateObject: RoomStateProtocol
}

2. 使用工厂方法创建状态

class Room {
    func changeState(to newState: RoomState) {
        self.state = newState
        
        // 工厂方法
        switch newState {
        case .preparing:
            self.stateObject = PreparingState()
        case .listing:
            self.stateObject = ListingState()
        case .auctioning:
            self.stateObject = AuctioningState()
        case .closed:
            self.stateObject = ClosedState()
        }
        
        addSystemMessage("房间状态变更为:(newState.displayName)")
    }
}

3. 记录状态转换日志

func changeState(to newState: RoomState) {
    let oldState = self.state
    self.state = newState
    
    // 记录状态转换
    print("🔄 状态转换:(oldState.displayName) → (newState.displayName)")
    
    // 可以添加到数据库或分析系统
    Analytics.trackStateChange(from: oldState, to: newState)
}

4. 验证状态转换的合法性

func changeState(to newState: RoomState) {
    // 验证转换是否合法
    guard isValidTransition(from: self.state, to: newState) else {
        print("⚠️ 非法的状态转换:(self.state) → (newState)")
        return
    }
    
    // 执行转换
    self.state = newState
    self.stateObject = createState(newState)
}

private func isValidTransition(from: RoomState, to: RoomState) -> Bool {
    let validTransitions: [RoomState: [RoomState]] = [
        .preparing: [.listing],
        .listing: [.auctioning],
        .auctioning: [.closed],
        .closed: [.preparing]
    ]
    
    return validTransitions[from]?.contains(to) ?? false
}

5. 提供状态查询接口

extension Room {
    var canStartAuction: Bool {
        return stateObject.startAuction(room: self)
    }
    
    var canPlaceBid: Bool {
        return state == .auctioning
    }
    
    var canUploadItem: Bool {
        return state == .preparing
    }
}

// 使用
if room.canPlaceBid {
    room.stateObject.placeBid(...)
}

6. 编写完整的单元测试

class StatePatternTests: XCTestCase {
    func testStateTransitions() {
        let room = Room(...)
        
        // 测试初始状态
        XCTAssertEqual(room.state, .preparing)
        
        // 测试状态转换
        room.stateObject.startAuction(room: room)
        XCTAssertEqual(room.state, .listing)
        
        // 等待自动转换
        wait(for: 3)
        XCTAssertEqual(room.state, .auctioning)
    }
    
    func testInvalidOperations() {
        let room = Room(...)
        
        // 在准备阶段不能出价
        let result = room.stateObject.placeBid(...)
        XCTAssertFalse(result)
    }
}

总结

何时使用状态模式

适合使用的场景

  1. 对象行为随状态改变而改变
  2. 有明确的状态转换规则
  3. 状态相关的代码较多
  4. 需要避免大量的条件判断

不适合使用的场景

  1. 状态很少(2-3个)
  2. 状态间没有明确的转换规则
  3. 状态逻辑非常简单
  4. 性能要求极高的场景

状态模式的价值

在拍拍房项目中,状态模式:

  1. 将复杂的业务流程结构化
    • 4个状态,4个类,清晰明了
    • 每个状态独立,互不干扰
  1. 提高代码质量
    • 避免了数百行的switch语句
    • 符合单一职责原则
    • 符合开闭原则
  1. 增强可维护性
    • 修改某个状态不影响其他状态
    • 新增状态只需添加新类
    • 状态转换一目了然
  1. 改善团队协作
    • 不同开发者可以独立开发不同状态
    • 减少Git冲突
    • 代码审查更容易
  1. 与权限中心完美配合
    • 状态负责"怎么做"
    • 权限负责"能不能做"
    • 职责清晰,耦合度低

最后的建议

  1. 不要过度设计:如果只有2-3个简单状态,可能不需要状态模式
  2. 绘制状态图:在实现之前先画出状态转换图
  3. 编写测试:为每个状态编写单元测试
  4. 文档化:记录每个状态的职责和转换规则
  5. 逐步重构:可以先用简单方式实现,再重构为状态模式

参考资源

设计模式相关

  • 《设计模式:可复用面向对象软件的基础》- GoF
  • 《Head First 设计模式》

本项目相关

5 分钟把 Coze 智能体嵌入网页:原生 JS + Vite 极简方案

作者 烟袅
2025年11月29日 17:49

你已经创建好了 Coze 智能体,现在想快速把它接入一个网页?不用 React、不用 Vue,甚至不用手敲 npm create —— 本文教你用 trae 的 AI 助手 + 原生 HTML/JS,5 分钟搭建一个可运行、可部署、安全调用 Coze OpenAPI 的前端 Demo。

我们将实现:

  • 通过 trae AI 一键生成项目并初始化 Vite
  • 安全注入 Bot ID 和 API Token
  • 调用 Coze 接口实现问答交互

一、用 trae AI 快速搭建项目(无需手动命令)

告别 npm init 和配置文件!我们借助 trae 的右侧 AI 对话栏,全自动完成项目创建。

操作步骤如下:

  1. 打开 trae 平台,进入任意工作区

  2. 在右侧 AI 对话框 中输入:

    创建一个通用的原生HTML/CSS/JS 项目
    
  3. 等待 AI 生成基础结构(通常包含 index.htmlmain.jsstyle.css

  4. 接着在同一对话中继续输入:

    帮我初始化vite配置
    
  5. AI 会自动为你:

    • 创建 vite.config.js
    • 添加 package.json 脚本(如 devbuild
    • 安装 vite 依赖(或提示你运行 npm install

✅ 此时你已拥有一个标准的 Vite 原生 JS 项目,无需任何手动配置!

将项目同步到本地后,执行:

npm run dev

确保页面能正常打开,接下来我们集成 Coze。


二、获取 Coze 智能体凭证

  1. 复制两个关键信息:

    • Bot ID 进入你的智能体,在链接最后那一串就是你的ID,选择复制

    • API Key 点击Web SDK 将其中的token复制下来

image.png

⚠️ 这个 API Key 具有调用权限,请务必保密!

关于智能体具体的创建 juejin.cn/post/757769… 这篇文章里面有,当然智能体发布的时候一定要选择API选项


三、安全注入环境变量

在项目根目录创建 .env.local 文件:

VITE_BOT_ID=your_actual_bot_id
VITE_API_KEY=your_actual_api_key

🔒 Vite 只会暴露以 VITE_ 开头的变量到客户端代码,这是官方推荐的安全做法。


四、编写前端交互逻辑

1. index.html

可以把trae生成的代码删掉用下面这份

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Coze API Demo</title>
  <script src="https://cdn.jsdelivr.net/npm/marked@4.3.0/lib/marked.umd.min.js"></script>
</head>
<body>
  <h1>Coze API Demo 随处智能</h1>
  <input type="text" id="ipt" placeholder="请输入问题">
  <div id="reply">think...</div>
  <script type="module" src="./script.js"></script>
</body>
</html>

在这段代码看起有点不一样

<script src="https://cdn.jsdelivr.net/npm/marked@4.3.0/lib/marked.umd.min.js"></script>

是哪里冒出来的呢?

其实加上这个主要是为了待会我们从智能体那里获取图片展示到网页上,如果不加的话我们只会获得图片的链接,这还要结合待会的js一起使用

2. main.js

const ipt = document.getElementById('ipt');
const reply = document.getElementById('reply');
const endpoint = 'https://api.coze.cn/open_api/v2/chat';
// DOM 2 
ipt.addEventListener('change',async function(event) {
  const prompt = event.target.value;
  console.log(prompt);
  const payload = {
    bot_id: import.meta.env.VITE_BOT_ID,
    user: 'yvo',
    query: prompt,
    chat_history:[],
    stream: false,
    custom_variables: {
      prompt: '你是一个AI助手'
    }
  }
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${import.meta.env.VITE_API_KEY}`
    },
    body: JSON.stringify(payload)
  })
  const data = await response.json();
  console.log(data, '////');
  // reply.innerHTML = data.messages[1].content;
  reply.innerHTML=marked.parse(data.messages.find(item => item.type === 'answer').content);
})

代码分析

  reply.innerHTML=marked.parse(data.messages.find(item => item.type === 'answer').content);

这段代码可能看起来有点突兀,那我们拆开来看首先我们看吧

data.messages.find(item => item.type === 'answer').content

这主要是获取智能体的回答,这时就有人问了一般获取信息不都是使用 .choices[0].message.content来获取吗?

但是coze的智能体返回的结构是不一样的

image.png

看这个结构很容易观察到其实coze智能体返回的结构需要在messages[1].content或type:"answer"才能拿到结果,这就是coze与我们调用一般的llm不一样的地方。

接下来我们继续分析

marked.parse()

将 Markdown 格式的字符串 → 转换成 HTML 字符串

这样浏览器才能正确显示标题、列表、链接、图片等内容。

这也就实现了我们能在页面上获取智能体给我们的图片了。 我们可以删去试试看效果

image.png

我们并没有得到我们想要的只获得了https地址

那加上试试呢?

image.png

成功将照片拿到。

 const payload = {
    bot_id: import.meta.env.VITE_BOT_ID,
    user: 'yvo',
    query: prompt,
    chat_history:[],
    stream: false,
    custom_variables: {
      prompt: '你是一个AI助手'
    }

这段代码好像见的也不多,这段其实就要根据www.coze.cn/open/docs/d… coze的官方文档去使用了


五、启动 & 验证

npm run dev

在浏览器输入问题(如“JavaScript 如何判断数组?”),即可看到 Coze 智能体的实时回复!


七、常见问题

Q:返回 {"code":4101,"msg":"The token you entered is incorrect"}
A:请检查:

  • .env.local 是否命名正确
  • Token 是否正确或过期

结语

通过 trae AI + Vite + Coze OpenAPI,我们用最轻量的方式实现了智能体前端集成。整个过程:

  • 无框架负担
  • 无复杂构建
  • 环境变量安全隔离
  • 代码清晰可维护

一个输入框,一行 API 调用,背后是千行训练数据与万亿参数的智能体在为你思考。
而你,只用了 5 分钟,就把它请进了自己的网页。
这不是魔法——这是新时代前端工程师的日常。

从「似懂非懂」到「了如指掌」:Promise 与原型链全维度拆解

2025年11月29日 17:36

前言

在前端世界里,Promise原型链(Prototype) 是两个看似毫不相干,却又互相影响、甚至能相互解释的重要概念。

很多人学习 Promise 时,会关注它的使用:thencatchfinallyPromise.all 等;
学习原型链时,又会关注 __proto__prototype、构造函数与实例之间的关系。

但鲜有人把 Promise 本身也是一个对象,它也依赖原型链运作 这件事真正联系起来讲透。

本文将以一次完整的 Promise 异步流程为主线,把“原型链 + 状态机 + 微任务”融合讲解,让你完全理解 Promise 到底是怎么在底层“跑”起来的。


一、Promise 为什么是“对象”?

我们常常写:

const p = new Promise((resolve, reject) => {
  setTimeout(() => resolve('OK'), 1000)
})

很多人知道 Promise 是“异步解决方案”,但忽略了一个基本事实:

Promise 是一个构造函数(类),你创建的是它的实例。

也就是说:

  • Promise —— 构造函数(带 prototype
  • p —— 实例对象(带 __proto__

打开控制台试试:

console.log(p.__proto__ === Promise.prototype) // true

这里马上就把原型链扯进来了。

🔍 Promise.prototype 上都有啥?

输入:

console.log(Promise.prototype)

你会看到:

then: ƒ then()
catch: ƒ catch()
finally: ƒ finally()
constructor: ƒ Promise()
...

这说明:

所有 Promise 实例都是通过原型链访问 then/catch/finally 的。

也就是说 p.then() 并不是实例自身有,而是:

p ---> Promise.prototype ---> Object.prototype ---> null

这为后文理解 Promise “链式调用”机制奠定基础。


二、原型链视角下,看懂 Promise 的执行流

我们直接看一个你提供的代码精简版:

const p = new Promise((resolve, reject) => {
  console.log(111)
  setTimeout(() => {
    reject('失败1')
  }, 1000)
})

console.log(222)

p.then(data => {
  console.log(data)
}).catch(err => {
  console.log(err)
}).finally(() => {
  console.log('finally')
})

输出顺序:

111
222
失败1
finally

要理解为什么 Promise 能这样执行,必须从两个角度讲:

  • (1)Promise 内部是状态机(pending → fulfilled / rejected)
  • (2)then/catch/finally 是通过原型链挂载的“回调注册器”

我们分开看看。


1)Promise 内部是一个状态机

内部状态(无法手动修改):

状态 描述 何时出现
pending 初始状态 执行 executor 期间
fulfilled resolve 被调用 成功
rejected reject 被调用 失败

也就是说:

new Promise(executor)

执行后:

  • 立即执行 executor
  • executor 只在同步阶段运行
  • 真正的 resolve/reject 回调是“挂起来”,等事件循环驱动

所以你看到:

111(executor 同步执行)
222(外部同步执行)
失败1(异步到点后 reject)
finally(状态 settled 后触发)

2)then/catch/finally:它们不是魔法,是原型链的方法

看看这段链式调用:

p.then(...).catch(...).finally(...)

为什么可以一直“链式”?

因为每次调用 then 都 返回一个新的 Promise 实例

p.then(...) → p2
p2.catch(...) → p3
p3.finally(...) → p4

这几个实例的原型链依然是:

p2.__proto__ === Promise.prototype
p3.__proto__ === Promise.prototype
...

因此:

链式本质 = 每次链式都返回一个新的 Promise 实例,然后继续在原型链上查找 then/catch/finally。

这就是原型链在 Promise 底层的重要性。


三、原型链的类比:Promise 就像“火车头 + 车厢”系统

你提到的类比非常棒,我把它整理成完整模型:

✨ Promise = 火车系统

  • 构造函数(Promise) = 火车制造厂
  • 原型对象(Promise.prototype) = “火车车厢模板”
  • 实例(p) = 火车头
  • then/catch/finally = 可以接在车头后的“车厢类型”

于是我们看到:

p(车头).then(挂一个车厢)
         .then(再挂一节)
         .catch(挂一个处理失败的车厢)
         .finally(挂尾部的清理车厢)

每次挂车厢(调用 then/catch)时,都会生成 新的火车车头(新的 Promise 实例)

整个火车最终沿着轨道(事件循环)开动到终点。

⚠️ 注意:为什么 finally 一定执行?

因为 finally 不关心结果,只关心火车是否开到终点(settled)。


四、Promise 与普通对象原型链的对比

你提供了一个经典例子:

function Person(name, age) {
  this.name = name
  this.age = age
}

Person.prototype.speci = '人类'

let zhen = new Person('白兰地空瓶', 18)
console.log(zhen.speci)

const kong = {
  name: '空瓶',
  hobbies: ['读书', '喝酒']
}

zhen.__proto__ = kong

console.log(zhen.hobbies, zhen.speci)

输出:

人类
['读书','喝酒'] undefined

这个例子非常适合用来对比 Promise 的原型链逻辑。

对比 1:实例可以动态改原型(不推荐)

zhen.__proto__ = kong 改掉了原来的 Person.prototype

所以:

  • 能访问 hobbies:因为来自 kong
  • 不能访问 speci:因为已脱离 Person.prototype

Promise 则不能做这种事

你不能这样做:

p.__proto__ = {}

否则:

p.then is not a function

因为 then/catch/finally 都来自 Promise.prototype。

这反而让我们更清楚地理解:

Promise 的能力几乎全部来自原型链。


五、Promise.all 的底层逻辑:一辆多车头的“联挂火车”

提到 Promise.all,这里正好顺便讲讲它的底层设计。

Promise.all([p1, p2, p3])

机制可以用一个形象类比解释:

  • 假设有三辆火车(p1/p2/p3)
  • Promise.all 创建一辆“总火车头” pAll
  • pAll 盯着三个火车头,只要全部变成 fulfilled,就把所有结果一次性返回
  • 如果有一个 reject,则整个 pAll 变成 rejected(列车脱轨)

也就是说:

Promise.all = 多个 Promise 状态机的并联 + 一个新的总状态机。

为什么它能做到?

答案依旧在原型链:

  • Promise.all 本质是一个静态方法,返回新的 Promise 实例
  • 新的 Promise 实例依然沿用同一套路(prototype → then/catch)

六、用真实工程场景收尾:Promise 原型链为什么重要?

在真实项目里,理解 Promise 的原型机制有三个实际价值:

① debugger 时能看清原型链,定位异步回调来源

你能区分:

  • then 回调从哪里来的?(Promise.prototype.then)
  • promise 链断在哪一层?

② 手写 Promise 时必须实现 then/catch/finally

如果你手写 Promise A+:

MyPromise.prototype.then = function(onFulfilled, onRejected) {}

这里你就必须自己处理链式、状态机、回调队列。

③ 能理解 async/await 的底层依赖 Promise 链式调度

await 会把后续步骤注册到 promise.then 中。

理解 then 的原型链,就能理解 async/await 的机制本质。


七、总结:Promise + 原型链的全景图

// 创建实例
const p = new Promise(executor)

// 原型链:调用能力来自这里
p.__proto__ = Promise.prototype

// 状态机:内部维护 pending → fulfilled/rejected

// then/catch/finally:注册微任务

// 链式调用:每次都返回一个新的 Promise 实例

// Promise.all:多个状态机的并联

一句话总结:

Promise 本质是一个基于“原型链 + 状态机 + 微任务队列”的异步调度框架。

它既是面向对象设计(通过原型链复用方法),又是异步控制核心工具(内部状态机)。

理解二者的融合,你就真正吃透了 Promise。

🧠 深入理解 JavaScript Promise 与 `Promise.all`:从原型链到异步编程实战

2025年11月29日 17:32

在现代 JavaScript 开发中,Promise 是处理异步操作的核心机制之一。ES6 引入的 Promise 极大地简化了“回调地狱”(Callback Hell)问题,并为后续的 async/await 语法奠定了基础。而 Promise.all 则是并发执行多个异步任务并统一处理结果的强大工具。

本文将结合 原型链原理Promise 基础用法实际示例代码,带你系统掌握 Promise 及其静态方法 Promise.all 的使用与底层逻辑。


🔗 一、JavaScript 的面向对象:原型链而非“血缘”

在深入 Promise 之前,我们先厘清一个关键概念:JavaScript 的继承不是基于“类”的血缘关系,而是基于原型(prototype)的链式查找机制

1.1 🏗️ 构造函数与原型对象

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.speci = '人类';

let zhen = new Person('张三', 18);
console.log(zhen.speci); // 输出: "人类"
  • Person 是构造函数。
  • Person.prototype 是所有 Person 实例共享的原型对象。
  • zhen.__proto__ 指向 Person.prototype
  • Person.prototype.constructor 又指回 Person,形成闭环。

🚂 小比喻:可以把 constructor 看作“车头”,prototype 是“车身”。实例通过 __proto__ 连接到车身,而车身知道自己的车头是谁。

1.2 ⚡ 动态修改原型链(不推荐)

const kong = {
    name: '孔子',
    hobbies: ['读书', '喝酒']
};

zhen.__proto__ = kong;
console.log(zhen.hobbies);     // ✅ 输出: ['读书', '喝酒']
console.log(kong.prototype);   // ❌ undefined!普通对象没有 prototype 属性

⚠️ 注意:

  • 只有函数才有 prototype 属性;
  • 普通对象(如 kong)只有 __proto__,没有 prototype
  • 在这里kong是object的一个实例kong.__prpto__ == object.prototype

💡 虽然可以动态修改 __proto__,但会破坏代码可预测性,影响性能,应避免使用。


⏳ 二、Promise:ES6 的异步解决方案

2.1 🧩 Promise 基本结构

<script>
const p = new Promise((resolve, reject) => {
    console.log(111); // 同步执行
    setTimeout(() => {
        console.log(333);
        // resolve('结果1');  // 成功
        reject('失败1');      // 失败
    }, 1000);
});

console.log(222);
console.log(p, '////////'); // 此时 p 状态仍是 pending
console.log(p.__proto__ == Promise.prototype); // true
</script>

📋 执行顺序分析:

  1. 111 立即输出(executor 函数同步执行)✅
  2. 222 紧接着输出 ✅
  3. p 此时处于 pending(等待) 状态 ⏳
  4. 1 秒后,333 输出,调用 reject('失败1'),状态变为 rejected
  5. .catch() 捕获错误,.finally() 无论成功失败都会执行 🔁

2.2 🎯 Promise 的三种状态

  • ⏳ pending:初始状态,既不是成功也不是失败。
  • ✅ fulfilled:操作成功完成(通过 resolve 触发)。
  • ❌ rejected:操作失败(通过 reject 触发)。

🔒 核心特性:一旦状态改变,就不可逆。这是 Promise 的设计基石。

2.3 🔍 原型关系验证

console.log(p.__proto__ === Promise.prototype); // ✅ true
  • pPromise 的实例。
  • 所有 Promise 实例的 __proto__ 都指向 Promise.prototype
  • Promise.prototype 上定义了 .then(), .catch(), .finally() 等方法。
  • Promise.prototype.__proto__ == object.prototype

🚀 三、Promise.all:并发处理多个异步任务

3.1 ❓ 什么是 Promise.all

Promise.all(iterable) 接收一个可迭代对象(如数组),其中包含多个 Promise。它返回一个新的 Promise:

  • ✅ 全部成功 → 返回一个包含所有结果的数组(顺序与输入一致)。
  • ❌ 任一失败 → 立即 rejected,返回第一个失败的原因。

3.2 💻 使用示例

const task1 = fetch('/api/user');       // 假设返回 { id: 1, name: 'Alice' }
const task2 = fetch('/api/posts');      // 假设返回 [{ title: 'JS' }]
const task3 = new Promise(resolve => setTimeout(() => resolve('done'), 500));

Promise.all([task1, task2, task3])
  .then(([user, posts, msg]) => {
    console.log('全部完成:', user, posts, msg);
  })
  .catch(err => {
    console.error('某个任务失败:', err);
  });

🌐 适用场景:需要同时加载用户信息、文章列表、配置数据等,全部就绪后再渲染页面。

3.3 ⚠️ 错误处理演示

const p1 = Promise.resolve('成功1');
const p2 = Promise.reject('失败2');
const p3 = Promise.resolve('成功3');

Promise.all([p1, p2, p3])
  .then(results => console.log('不会执行'))
  .catch(err => console.log('捕获错误:', err)); // 输出: "失败2"

关键点:只要有一个失败,整个 Promise.all 就失败,其余成功的 Promise 结果会被丢弃。

3.4 🛡️ 替代方案:Promise.allSettled(ES2020)

如果你希望无论成功失败都等待所有任务完成,可以使用 Promise.allSettled

Promise.allSettled([p1, p2, p3])
  .then(results => {
    results.forEach((res, i) => {
      if (res.status === 'fulfilled') {
        console.log(`✅ 任务${i} 成功:`, res.value);
      } else {
        console.log(`❌ 任务${i} 失败:`, res.reason);
      }
    });
  });

✅ 适用于:批量上传、日志收集、非关键资源加载等场景。


📚 四、总结:从原型到实践

概念 说明
🔗 原型链 JS 对象通过 __proto__ 查找属性,constructor 指回构造函数
Promise 表示异步操作的最终完成或失败,具有 pending/fulfilled/rejected 三种状态
🧩 Promise.prototype 所有 Promise 实例的方法来源(.then, .catch 等)
🚀 Promise.all 并发执行多个 Promise,全成功则成功,任一失败则整体失败
🛡️ 最佳实践 使用 Promise.all 提升性能;用 allSettled 处理非关键任务

💭 五、思考题

  1. 🤔 为什么 console.log(p)setTimeout 之前打印时,状态是 pending
  2. 🛠️ 能否通过修改 Promise.prototype.then 来全局拦截所有 Promise 的成功回调?这样做有什么风险?
  3. 📦 如果 Promise.all 中传入空数组 [],结果会是什么?

💡 答案提示

  1. 因为异步任务尚未执行,状态未改变。
  2. 技术上可行,但会破坏封装性、可测试性和团队协作,强烈不推荐
  3. 立即 resolved,返回空数组 [] —— 这是符合规范的!

通过本文,你不仅掌握了 PromisePromise.all 的用法,还理解了其背后的 原型机制异步执行模型。这将为你编写健壮、高效的异步代码打下坚实基础。🌟

Happy Coding! 💻✨

北京AI产业规模今年将超4500亿元

2025年11月29日 17:06
北京市科学技术委员会、中关村科技园区管委会今天(11月29日)正式发布《北京人工智能产业白皮书(2025)》。《白皮书》数据显示,2025年上半年,北京全市人工智能核心产业规模2152.2亿元,同比增长25.3%。初步估算2025年全年,产业规模有望超过4500亿元。(财联社)

国家航天局设立商业航天司 持续推动商业航天高质量发展

2025年11月29日 16:57
据国家航天局消息,该局已于近期设立商业航天司,相关业务正在逐步开展,标志着我国商业航天产业迎来专职监管机构,未来将持续推动我国商业航天高质量发展,产业链有望全线受益。记者了解到,国家航天局近日公布推进商业航天高质量安全发展行动计划(2025—2027年),提出将商业航天纳入国家航天发展总体布局,加快形成航天新质生产力,实现航天发展效能整体提升,有力支撑航天强国建设。这项计划明确,到2027年,商业航天产业生态高效协同,科研生产安全有序,产业规模显著壮大,创新创造活力显著增强,资源能力实现统筹建设和高效利用,行业治理能力显著提升,基本实现商业航天高质量发展。(财联社)

央行:继续坚持对虚拟货币的禁止性政策 持续打击虚拟货币相关非法金融活动

2025年11月29日 16:57
中国人民银行2025年11月28日召开打击虚拟货币交易炒作工作协调机制会议。会议要求,各单位要坚持以习近平新时代中国特色社会主义思想为指导,全面落实党的二十大和二十届历次全会精神,把防控风险作为金融工作的永恒主题,继续坚持对虚拟货币的禁止性政策,持续打击虚拟货币相关非法金融活动。各单位要深化协同配合,完善监管政策和法律依据,聚焦信息流、资金流等重点环节,加强信息共享,进一步提升监测能力,严厉打击违法犯罪活动,保护人民群众财产安全,维护经济金融秩序稳定。(财联社)

从摄影新手到三维光影师:Three.js 核心要素的故事

作者 一千柯橘
2025年11月29日 16:43

当我第一次学习摄影时,老师告诉我一句话:

“你不是在拍东西,而是在拍光。”

后来我学习 Three.js 时突然意识到:
这句话原来依旧成立。

Three.js 不只是一个 3D 引擎,更像是一台虚拟相机。要拍好这张“虚拟的照片”,我们必须掌握三个核心要素:

场景(Scene)

相机(Camera)
灯光与材质(Light & Material)

于是,我把学习过程想象成一个摄影新手成长为三维光影师的故事。

空无一物的影棚 —— Scene 场景

故事从一个空影棚开始。

当我第一次打开 Three.js 时,教程告诉我:

const scene = new THREE.Scene();

这就像摄影师走进了一个空旷的工作室:
没有布景、没有模特、没有灯光,甚至连相机都还没架好, 在影棚这个场景中,摄影师可以在这个场景中放任何的东西:

  • 架好摄像机(Camera 📹)
  • 拍照的物体(Mesh 网格物体)、物体拥有着自己的形状(Geometry几何体)和材质(Material)
  • 摆设好灯光(Light)
  • 也可以是任意的对象 (Object3D)

摄影师往 Scene 里布置道具,而程序员的你往 Scene 里添加各种对象,因此 场景就是一个可以放任何东西的容器

找到你要观看的角度 —— Camera 相机

刚学摄影时,我最常做的事情,就是移动、蹲下、趴着、绕圈……
只为了找到一个“对的角度”。

Three.js 的相机就是你的眼睛。创建相机就像准备拍摄时拿起单反:

const camera = new THREE.PerspectiveCamera(const camera = new THREE.PerspectiveCamera(
  50, // 相机视野角度,摄像机的视野角度越大,摄像机看到的场景就越大,反之越小
  window.innerWidth / window.innerHeight, // 宽高比
  0.1, // 近平面(近端渲染距离),指定从距离相机多近的位置开始渲染,推荐默认值0.1
  1000 // 远平面(远端渲染距离)指定摄像机从它所在的位置最远能看到多远,太小场景中的远处物体会看不见,太大会浪费资源影响性能,推荐默认值1000
);

// 2.1 设置相机的位置,放在不同的位置看到的风景当然不一样
camera.position.set(5, 10, 10); // x, y, z

camera.lookAt(0, 0, 0); // 设置相机方向(这就是你女朋友让你找最佳角度的原因)

摄影师会说:“我走两步,让模特在背景中更突出。”
程序员会说:

camera.position.z = 3;
camera.lookAt(0, 0, 0)

本质完全一样:
都是在调整观察世界的方式。

让世界真正亮起来 —— Light & Material 灯光与材质

你可以有再漂亮的模特、再好的相机,如果没有光——
一切都会变成漆黑一片。

Three.js 也是如此。你搭了一个完美的 3D 模型,如果没有光,它看起来只是纯黑。

于是我制作“虚拟布光”:

const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 10, 5);
scene.add(light);

摄影师打灯,而我在代码里放置光源:

  • DirectionalLight(平行光)= 太阳光
  • PointLight(点光源)= 想象灯泡发光,由点向八方发射
  • SpotLight(聚光灯)= 舞台灯,从上打下来,呈现圆锥体,它离光越远,它的尺寸就越大。这种光源会产生阴影
  • AmbientLight(环境光)= 影棚柔光,环境光没有特定的来源方向,不会产生阴影

同时材质(Material)也等同于现实世界的“被光击中时的反应”:

  • 皮肤 = standard material
  • 金属 = metalness 高
  • 塑料 = roughness 较高
  • 玻璃 = transparent=True + envMap

想要一个皮肤质感的物体?
那么你就得给材质加入 roughness、metalness、normalMap 就像摄影师在打柔光,为人物皮肤创造质感。

光与材质的搭配,就是 Three.js 里的“布光艺术”。

最终章:按下快门 —— Renderer 渲染器

当场景布好、相机调好、灯光到位后——
摄影师要做的就是按下快门。

在 Three.js 里:

renderer.render(scene, camera);

渲染器就是那个“快门”,
真正把世界投射到屏幕上。

摄影师用快门把现实世界的光记录下来;
Three.js 用 GPU 把虚拟世界的光影计算出来。

本质上,两者做的是同一件事:

把真实或虚拟的三维世界,投射成一张二维图像。

import * as THREE from "three";

// 1. 创建场景
const scene = new THREE.Scene();

// 2. 创建相机(透视投影相机)
const camera = new THREE.PerspectiveCamera(
  50, // 相机视野角度,摄像机的视野角度越大,摄像机看到的场景就越大,反之越小
  window.innerWidth / window.innerHeight, // 宽高比
  0.1, // 近平面(近端渲染距离),指定从距离相机多近的位置开始渲染,推荐默认值0.1
  1000 // 远平面(远端渲染距离)指定摄像机从它所在的位置最远能看到多远,太小场景中的远处物体会看不见,太大会浪费资源影响性能,推荐默认值1000
);

// 2.1 设置相机的位置
camera.position.set(5, 10, 10); // x, y, z

camera.lookAt(0, 0, 0); // 设置相机方向(默认看向场景原点)

// 3. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true }); // 开启抗锯齿,使边缘更平滑
// 3.1 设置渲染器的大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 3.2 将渲染器的canvas内容添加到body
document.body.appendChild(renderer.domElement);

// 4. 创建一个立方体几何体
const geometry = new THREE.BoxGeometry(4, 4, 4); // 宽、高、深

// 为了让光源有效果,我们使用 MeshLambertMaterial 或 MeshPhongMaterial
//  创建材质 MeshLambertMaterial (兰伯特材质) 是一种非光泽材质,会受光照影响,但没有镜面高光
const material = new THREE.MeshLambertMaterial({
  color: 0x00ff00, // 颜色
  // wireframe: true, // 如果需要线框效果可以加上
});

// 6. 创建一个网格模型(网格模型由几何体和材质组成)
// Mesh 构造函数通常只接受一个材质。如果需要多材质,Three.js 有专门的 MultiMaterial 或 Group 来处理
const cube = new THREE.Mesh(geometry, material); // 使用 MeshLambertMaterial

// 6.1 将几何模型添加到场景中
scene.add(cube);

// 6.2 设置相机看向物体(拍摄对象)的位置(默认状态下相机看向的是场景的原点(0,0,0))
camera.lookAt(cube.position);

// 7. 创建光源
const spotLight = new THREE.SpotLight(0xffffff); // 创建聚光灯,颜色为白色
// 7.1 设置光源的位置
spotLight.position.set(0, 20, 20); // 调整光源位置,使其能够照亮立方体
// 7.2 设置光源照射的强度,默认值为1, 越大越亮
spotLight.intensity = 2;
// 7.3 将光源添加到场景中
scene.add(spotLight);

// 8. 为了方便观察 3D 图像,添加三维坐标系对象
const axesHelper = new THREE.AxesHelper(6); // 参数表示坐标系的大小 (x轴红色, y轴绿色, z轴蓝色)
scene.add(axesHelper); // 将坐标系添加到场景中

// 9. 渲染函数
function animate() {
  requestAnimationFrame(animate); // 请求再次执行渲染函数animate,形成循环

  // 让立方体动起来
  cube.rotation.x += 0.01; // 沿x轴旋转
  cube.rotation.y += 0.01; // 沿y轴旋转
  cube.rotation.z += 0.01;

  renderer.render(scene, camera); // 使用渲染器,通过相机将场景渲染出来
}

animate(); // 执行渲染函数,进入无限循环,完成渲染

2025年CSS新特性大盘点

作者 Immerse
2025年11月29日 16:41

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

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

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

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

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

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


2025年了,CSS又进化了

去年写过一篇 CSS 新特性盘点,本来以为今年不会有太大变化。结果一看,新东西比去年还多。

这次整理了几个我觉得特别实用的功能,浏览器支持也都不错,可以用起来了。

终于可以动画到 auto 了

之前我们做高度展开动画,基本都是靠 max-height 硬撑。

比如从 0 展开到实际高度,只能写个超大的值,体验很差。

现在可以直接动画到 auto 了:

html {
  interpolate-size: allow-keywords;
}

加上这一行,所有 height: 0 到 height: auto 的过渡都能生效。

或者你也可以用 calc-size() 函数,不需要全局设置:

.content {
  height: 3lh;
  overflow: hidden;
  transition: height 0.2s;

  &.expanded {
    height: calc-size(auto, size);
  }
}

这个功能总算来了。

而且不只是 height,任何接受尺寸的属性都能用,不只是 auto,min-content 这些关键字也行。

目前 Chrome 已经支持,其他浏览器应该也快了。

Popover 和 Invoker

Popover 是个 HTML 属性,给任意元素加上就有开关功能。

配合 Invoker 用起来更爽,不用写 JavaScript 就能控制弹窗。

<button commandfor="menu" command="toggle">
  打开菜单
</button>

<div id="menu" popover>
  菜单内容
</div>

这样就够了,按钮点击自动控制弹窗显示隐藏。

浏览器会自动处理无障碍访问、键盘操作、焦点管理这些细节。

而且还能配合 Anchor Positioning 用,让弹窗自动定位到触发元素旁边。

Popover 已经全浏览器支持,Invoker 目前只有 Chrome,不过有 polyfill 可以用。

CSS 里可以写函数了

CSS 有 calc()、clamp() 这些内置函数,现在我们可以自己写了:

@function --titleBuilder(--name) {
  result: var(--name) " is cool.";
}

然后就能在任何地方调用:

.title::after {
  content: --titleBuilder("CSS");
}

这个功能让 CSS 更像编程语言了。

把复杂逻辑封装到函数里,代码更清爽,也更好维护。

不过目前只有 Chrome 支持,可以先用着,不支持的浏览器会回退到默认值。

if() 函数也来了

CSS 本来就有很多条件逻辑,比如选择器匹配、媒体查询。

但这次的 if() 函数是第一个专门做条件分支的:

.grid {
  display: grid;
  grid-template-columns:
    if(
      media(max-width > 300px): repeat(2, 1fr);
      media(max-width > 600px): repeat(3, 1fr);
      media(max-width > 900px): repeat(auto-fit, minmax(250px, 1fr));
      else: 1fr;
    );
}

看起来像不像 switch 语句?第一个匹配的条件会生效。

条件可以是 media()、supports()、style() 这几种。

把所有逻辑都写在一个属性里,代码可读性好很多。

目前 Chrome 独占,其他浏览器还在路上。

表单输入框自动调整大小

field-sizing 这个属性专门解决表单输入框的问题。

textarea {
  field-sizing: content;
}

加上这一行,textarea 会自动根据内容调整高度。

用户输入多少内容,输入框就有多高,不用手动拖拽了。

在手机上体验特别好,拖拽调整大小本来就很难操作。

这个功能之前都是用 JavaScript 实现,现在 CSS 原生支持了。

Chrome 和 Safari 都能用,Firefox 估计也快了。

select 下拉框终于能自定义样式了

select 元素的外观一直很难自定义,打开后显示的选项更是完全没法控制。

现在可以完全自定义了,只要先开启:

select,
::picker(select) {
  appearance: base-select;
}

然后想怎么改就怎么改,选项的样式、布局、动画都能控制。

目前 Chrome 独占,不过不支持的浏览器会回退到原生样式,完全不影响使用。

text-wrap 让排版更好看

text-wrap: balance 可以让每行文字长度尽量接近:

h1 {
  text-wrap: balance;
}

用在标题上效果特别好,不会出现最后一行只有一个词的情况。

还有个 text-wrap: pretty,专门优化正文排版:

p {
  text-wrap: pretty;
}

浏览器会自动调整断行,避免孤词,让文字看起来更舒服。

balance 已经全浏览器支持,pretty 在 Chrome 和 Safari 能用。

这种优化对用户体验很重要,而且完全不影响功能,可以直接加上。

linear() 实现复杂缓动效果

CSS 的 linear 关键字之前就是匀速动画,很无聊。

但 linear() 函数可以实现超复杂的缓动,比如弹跳效果:

.bounce {
  animation-timing-function: linear(
    0, 0.004, 0.016, 0.035, 0.063, 0.098, 0.141 13.6%, 0.25, 0.391, 0.563, 0.765,
    1, 0.891 40.9%, 0.848, 0.813, 0.785, 0.766, 0.754, 0.75, 0.754, 0.766, 0.785,
    0.813, 0.848, 0.891 68.2%, 1 72.7%, 0.973, 0.953, 0.941, 0.938, 0.941, 0.953,
    0.973, 1, 0.988, 0.984, 0.988, 1
  );
}

这种效果用 cubic-bezier() 根本做不出来。

而且已经全浏览器支持了,可以放心用。

有在线工具可以生成这些值,不用自己手写。

shape() 函数画任意图形

CSS 之前有 path() 函数,但语法很难写,而且只能用像素。

shape() 是专门为 CSS 设计的,支持所有单位和自定义属性:

.arrow {
  clip-path: shape(
    evenodd from 97.788201% 41.50201%,
    line by -30.839077% -41.50201%,
    curve by -10.419412% 0% with -2.841275% -3.823154% / -7.578137% -3.823154%,
    smooth by 0% 14.020119% with -2.841275% 10.196965%,
    close
  );
}

可以用在 clip-path 裁剪元素,也能用在 offset-path 做路径动画。

而且可以响应式调整,配合媒体查询和容器查询都没问题。

Chrome 和 Safari 已经支持,Firefox 也在开发中。

attr() 变强了

之前 attr() 只能取字符串,现在可以指定类型了:

<div data-count="42" data-color="#ff0000">
div {
  --count: attr(data-count type(<number>));
  --color: attr(data-color type(<color>));
}

这样可以直接把 HTML 属性当数字或颜色用,方便多了。

目前 Chrome 独占,不过对于不支持的浏览器,可以设置回退值。

reading-flow 解决 Tab 顺序问题

用 Grid 或 Flexbox 重新排列元素后,Tab 键的焦点顺序会乱。

现在可以用 reading-flow 告诉浏览器按照视觉顺序来:

.grid {
  reading-flow: grid-rows;
}

这样焦点就会按照 Grid 的行顺序移动,不会乱跳了。

Flexbox 用 flex-flow,其他布局也有对应的值。

这个功能对无障碍访问很重要,不过目前只有 Chrome 支持。

等其他浏览器跟进之前,最好不要大量重排布局。

值得期待的功能

还有一些功能在开发中,但还没正式发布:

Masonry 布局虽然各浏览器实现不同,但在稳步推进。

Safari 的 random() 函数可以生成随机数,玩起来很有意思。

margin-trim 可以自动去掉容器边缘元素的外边距,Safari 独占中。

sibling-index() 和 sibling-count() 函数在 Chrome 能用,做交错动画很方便。

View Transitions 的 match-element 不用给每个元素起名字了,而且 Firefox 也在开发中。

还有很多其他功能在路上。

别忘了这些已经能用的

Container Queries 和 :has() 这些去年的新功能,现在已经全浏览器支持。

View Transitions、Anchor Positioning、Scroll-Driven Animations 也都在 Safari 上线了。

dvh 这些视口单位也成为标准了。

CSS 现在能做的事情越来越多,写起来也越来越顺手。

参考:frontendmasters.com/blog/what-y…

其他好文推荐

2025 最新!独立开发者穷鬼套餐

Windows 安装 Claude Code 的新姿势,保姆级教程

Claude Code 从入门到精通:最全配置指南和工具推荐

Claude Code 终极配置指南:一行命令搞定各种配置

一个配置文件搞定!Claude Code 多模型智能切换

这个 361k Star 的项目,一定要收藏!

搞定 XLSX 预览?别瞎找了,这几个库(尤其最后一个)真香!

【完整汇总】近 5 年 JavaScript 新特性完整总览

关于 Node,一定要学这个 10+万 Star 项目!

【翻译】使用 React 19 操作构建可复用组件

2025年11月29日 16:40

使用 React 19 Actions 构建可复用的 React 组件,通过 useTransition()useOptimistic() 实现功能。通过实际案例学习如何追踪待处理状态、实现乐观更新,并在 Next.js 应用路由器中暴露动作属性以实现自定义逻辑。

作者:Aurora Scharff

首发于 aurorascharff.no

React 19 Actions 简化了待处理状态、错误、乐观更新和顺序请求的处理。本文将探讨如何在 Next.js App Router 中使用 React 19 Actions 构建可复用组件。我们将利用 useTransition() 追踪过渡状态,使用 useOptimistic() 向用户提供即时反馈,并暴露 action 属性以支持父组件中的自定义逻辑。

React 19 Actions

根据更新后的 React 文档,动作(Actions)是在过渡(Transitions)内部调用的函数。过渡可以更新状态并执行副作用,相关操作将在后台执行,不会阻塞页面上的用户交互。过渡内部的所有动作都会被批量处理,组件仅在过渡完成时重新渲染一次。

Action 可用于自动处理待定状态、错误、乐观更新及顺序请求。在 React 19 表单中使用 <form action={} 属性时,以及向 useActionState() 传递函数时,也会自动创建动作。有关这些 API 的概述,请参阅我的 React 19 速查表或官方文档。

使用 useTransition() 钩子时,您还将获得一个 pending 状态,这是一个布尔值,用于指示过渡是否正在进行。这有助于在过渡过程中显示加载指示器或禁用按钮。

const [isPending, startTransition] = useTransition(); 
const updateNameAction = () => { 
  startTransition(async () => { 
    await updateName(); 
  }) 
})

此外,在钩子版本的 startTransition() 中调用的函数抛出的错误将被捕获,并可通过错误边界进行处理。

Action函数是常规事件处理的替代方案,因此应相应地命名。否则,该函数的使用者将无法明确预期其行为类型。

用例:路由器选择组件

假设我们要构建一个可复用的下拉菜单组件,该组件会将下拉菜单选中的值设置为URL中的参数。其实现方式可能如下所示:

export interface RouterSelectProps { 
  name: string; 
  label?: string; 
  value?: string; 
  options: Array<{ value: string; label: string }>; 
} 

export const RouterSelect = React.forwardRef<HTMLSelectElement, RouterSelectProps>(   
  function Select({ name, label, value, options, ...props }, 
    ref 
) { 
... 
return ( 
  <div> 
    {label && <label htmlFor={name}>{label}</label>} 
      <select 
        ref={ref} 
        id={name} 
        name={name} 
        value={value} 
        onChange={handleChange} 
        {...props} 
      > 
        {options.map((option) => ( 
           <option key={option.value} value={option.value}> 
             {option.label} 
           </option> 
         ))} 
      </select> 
  </div> 
  ) 
}

它可能会这样处理变化:

const handleChange = async ( 
  event: React.ChangeEvent<HTMLSelectElement> 
) => { 
  const newValue = event.target.value; 
  
  // Update URL 
  const url = new URL(window.location.href); 
  url.searchParams.set(name, newValue); 
  
  // Simulate a delay that would occur if the route destination is doing async work 
  await new Promise((resolve) => setTimeout(resolve, 500)); 
  
  // Navigate 
  router.push(url.href, { scroll: false }); 
};

可通过路由器传递 searchParams 来使用:

<RouterSelect
  name="lang" 
  options={Object.entries(languages).map(([value, label]) => { 
    return { value, label, }; 
  })} 
  label="Language" 
  value={searchParams.lang} 
/>

由于我们使用 Next.js 应用路由器,当延迟推送到路由时,下拉框的值不会立即更新,而是等到 router.push() 完成且搜索参数更新后才会刷新。

这会导致糟糕的用户体验:用户必须等待路由推送完成才能看到下拉框的新值,可能因此产生困惑,误以为下拉框功能失效。

使用Action追踪待处理状态

让我们创建一个使用 useTransition() 钩子的 Action 来追踪推送至路由器的状态。

我们将向路由器的推送封装在返回的 startNavTransition() 函数中,该函数将追踪该转场的待处理状态。这将使我们能够知道转场的进展以及何时完成。

  const [isNavPending, startNavTransition] = useTransition(); 
  const handleChange = async ( 
    event: React.ChangeEvent<HTMLSelectElement> 
  ) => { 
    const newValue = event.target.value; 
    startNavTransition(async () => { 
      const url = new URL(window.location.href); 
      url.searchParams.set(name, newValue); 
      await new Promise((resolve) => setTimeout(resolve, 500)); 
      router.push(url.href, { scroll: false }); 
    }); 
  };

现在,我们可以利用 isNavPending 状态在过渡过程中显示加载指示器,并添加 aria-busy 等辅助功能属性。

<div> 
  {label && <label htmlFor={name}>{label}</label>} 
  <select 
    ref={ref} 
    id={name} 
    name={name} 
    aria-busy={isNavPending} 
    value={value} 
    onChange={handleChange} 
    {...props} 
  > 
    {options.map((option) => ( 
      <option key={option.value} value={option.value}> 
        {option.label} 
      </option> 
    ))}
  </select> 
  {isNavPending && 'Pending nav...'} 
</div>

现在,用户将收到关于其与下拉菜单交互的反馈,不会认为它无法正常工作。

然而,下拉菜单仍然无法立即更新。

使用 useOptimistic() 添加乐观更新

此时就需要用到 useOptimistic() 函数。它允许我们立即更新状态,同时仍能追踪过渡的待处理状态。我们可以在过渡内部调用它:

const [optimisticValue, setOptimisticValue] = useOptimistic(value); 

const handleChange = async ( 
  event: React.ChangeEvent<HTMLSelectElement> 
) => { 
  const newValue = event.target.value; 
  startNavTransition(async () => { 
    setOptimisticValue(newValue); 
    const url = new URL(window.location.href); 
    url.searchParams.set(name, newValue); 
    await new Promise((resolve) => setTimeout(resolve, 500)); 
    router.push(url.href, { scroll: false }); 
  }); 
};

在过渡期间,optimisticValue 将作为临时客户端状态,用于立即更新下拉菜单。过渡完成后,optimisticValue 将最终更新为路由器返回的新值。

现在,我们的下拉菜单实现了即时更新,用户在过渡过程中即可看到菜单中的新值。

暴露Action属性

假设作为 RouterSelect 的用户,我们希望在选项变更时执行额外逻辑。例如,可能需要更新父组件中的其他状态或触发副作用。此时可暴露一个在选项变更时执行的函数。

参照 React 文档,我们可以向父组件暴露一个 action 属性。由于暴露的是 Action,命名时应符合规范,以便组件使用者明确预期行为。

具体实现如下:

export interface RouterSelectProps { 
  name: string; 
  label?: string; 
  value?: string; 
  options: Array<{ value: string; label: string }>; 
  setValueAction?: (value: string) => void; 
}

我们可以在handleChange过渡中调用此属性:

const handleChange = async ( 
  event: React.ChangeEvent<HTMLSelectElement> 
) => { 
  const newValue = event.target.value; 
  startNavTransition(async () => { 
    setOptimisticValue(newValue); 
    setValueAction?.(newValue); '
    const url = new URL(window.location.href); 
    url.searchParams.set(name, newValue); 
    await new Promise((resolve) => setTimeout(resolve, 500)); 
    router.push(url.href, { scroll: false }); 
  });
};

我们还应支持 async 函数。这使得操作回调既可以是同步的,也可以是异步的,而无需额外使用 startTransition 来包裹操作中的 await 语句。

export interface RouterSelectProps { 
  ...// other props 
  setValueAction?: (value: string) => void | Promise<void>; 
}

然后只需 await 操作完成,再推送到路由器:

const handleChange = async ( 
  event: React.ChangeEvent<HTMLSelectElement> 
) => { 
  const newValue = event.target.value; 
  startNavTransition(async () => { 
    setOptimisticValue(newValue); 
    await setValueAction?.(newValue); 
    ... // Push to router 
  }); 
};

在父组件中使用 Action 属性

现在,我们可以通过 setValueAction 属性执行状态更新,并且由于命名规范,我们清楚会行为的结果。

例如,如果我们使用 useState() 设置一条消息:

const [message, setMessage] = useState(''); 
return ( 
  <> 
  <div> 
    Message: {message} <br /> 
  </div> 
  <RouterSelect 
    setValueAction={(value) => { 
      setMessage(`You selected ${value}`);
    }}

我们知道,此状态更新将在向路由器推送完成后发生。

此外,若现在需要乐观更新,可调用 useOptimistic()

const [message, setMessage] = useState(''); 
const [optimisticMessage, setOptimisticMessage] = useOptimistic(message); 

return ( 
  <> 
  <div> 
    Message: {message} <br /> 
    Optimistic message: {optimisticMessage} 
  </div> 
  <RouterSelect 
    setValueAction={(value) => { 
      setOptimisticMessage(`You selected ${value}`); 
      setMessage(`You selected ${value}`); 
    }}

我们知道此状态更新将立即发生。

最终的select实现如下所示:

'use client'; 

... 
export interface RouterSelectProps { 
  name: string; 
  label?: string; 
  value?: string | string[]; 
  options: Array<{ value: string; label: string }>; 
  setValueAction?: (value: string) => void | Promise<void>; 
} 

export const RouterSelect = React.forwardRef<HTMLSelectElement, RouterSelectProps>( 
  function Select( 
    { name, label, value, options, setValueAction, ...props }, 
    ref 
  ) { 
    const router = useRouter(); 
    const [isNavPending, startNavTransition] = React.useTransition(); 
    const [optimisticValue, setOptimisticValue] = React.useOptimistic(value); 
    
    const handleChange = async ( 
      event: React.ChangeEvent<HTMLSelectElement> 
    ) => { 
      const newValue = event.target.value; 
      startNavTransition(async () => { 
        setOptimisticValue(newValue); 
        await setValueAction?.(newValue); 
        const url = new URL(window.location.href); 
        url.searchParams.set(name, newValue); 
        await new Promise((resolve) => setTimeout(resolve, 500)); 
        router.push(url.href, { scroll: false }); 
      }); 
    }; 
    
    return ( 
      <div> 
        {label && <label htmlFor={name}>{label}</label>} 
        <select 
          ref={ref} 
          id={name} 
          name={name} 
          value={optimisticValue} 
          onChange={handleChange} 
          {...props} 
        > 
          {options.map((option) => ( 
            <option key={option.value} value={option.value}> 
              {option.label} 
            </option> 
          ))} 
          </select> 
          {isNavPending && 'Pending nav...'} 
      </div> 
    ); 
  } 
);

查看这个StackBlitz,获取一个可运行的示例。

若需查看本文所述模式的更实用、更贴近实际的应用示例,请参阅我Next.js 15 Conferences项目中的Filters.tsx组件。

构建复杂、可重用的组件

在构建更复杂的可复用组件时,我们可能会遇到限制,迫使我们将乐观更新等逻辑移至父组件。

以我尝试的Ariakit示例为例,显示值的生成必须在可复用选择组件外部完成。这意味着我们无法在可复用选择组件内部调用 useOptimistic 。为解决此问题,可暴露 setValueAction 属性,然后在父组件中调用 useOptimistic() 立即更新状态。

通过这种方式,既能保持组件复用性,又允许父组件实现自定义Action逻辑。

关键要点

  • 动作是在过渡中调用的函数,可更新状态并执行副作用。
  • useTransition() 提供待处理状态以追踪过渡进度。
  • useOptimistic() 允许在过渡中立即更新状态。
  • 向可复用组件暴露动作属性,可在父组件中实现自定义逻辑。
  • 在父组件中使用 useOptimistic() 可立即更新状态,同时保持组件的复用性。
  • 动作的命名对向组件使用者传达预期行为至关重要。

结论

在本篇博文中,我们探讨了如何利用 React 19 动作构建可复用组件,追踪过渡状态,采用乐观更新策略,并暴露动作属性以实现自定义逻辑。我们演示了 useTransition() 如何提供待处理状态以优化用户反馈,useOptimistic() 如何实现即时 UI 更新,以及暴露动作属性如何在保持组件复用性的同时允许父组件执行自定义逻辑。

通过遵循动作命名规范并运用 React 的并发特性,我们能够构建出复杂度极低却能提供流畅用户体验的组件。

源码

当我把 proto 打印出来那一刻,我懂了JS的原型链

作者 栀秋666
2025年11月29日 16:19

💬 前言:我本以为我会面向对象,结果我连“对象”都没搞懂

刚开始学 JavaScript 的时候,我以为:

function Person(name) {
  this.name = name;
}

const p1 = new Person('小明');
console.log(p1.name); // 小明

这不就是面向对象吗?简单!

直到有一天,我在控制台敲下:

console.log(p1.__proto__);

然后——我的世界崩塌了。

满屏的 [[Prototype]]constructor__proto__……我仿佛掉进了一个无限嵌套的俄罗斯套娃里。

“我是谁?”
“我从哪里来?”
“我要到哪里去?”
——来自一个被原型链逼疯的学生的灵魂三问。

今天,就用一个真实学习者的视角,带你从困惑到理解,一步步揭开 prototype 的神秘面纱。没有高深术语,只有大白话 + 可运行代码 + 我踩过的坑。


🚪 一、为什么需要原型?—— 因为我不想每个对象都背一份方法

假设我们要创建多个学生对象:

❌ 错误写法:每个学生都自带“技能包”

function Student(name) {
  this.name = name;
  // 每个学生都独立拥有一个 sayHello 方法
  this.sayHello = function() {
    console.log(`大家好,我是${this.name}`);
  };
}

const s1 = new Student('张三');
const s2 = new Student('李四');

console.log(s1.sayHello === s2.sayHello); // false → 完全不同的两个函数!

问题来了:如果创建 1000 个学生,就会有 1000 个 sayHello 函数,内存直接爆炸 💥。

这就像学校给每个学生发一本《礼仪手册》,其实大家看的都是同一本书,但每人一本——太浪费了!

✅ 正确姿势:把公共方法放进“共享书架”(prototype)

function Student(name) {
  this.name = name; // 每个学生独有的属性
}

// 所有学生共享的方法,统一挂载到 prototype 上
Student.prototype.sayHello = function() {
  console.log(`大家好,我是${this.name}`);
};

const s1 = new Student('张三');
const s2 = new Student('李四');

console.log(s1.sayHello === s2.sayHello); // true → 同一个函数,只存一份!
s1.sayHello(); // 大家好,我是张三
s2.sayHello(); // 大大家好,我是李四

📌 我的理解

  • prototype 就是构造函数的“共享书架”
  • 实例自己没有的方法,会自动去书架上找
  • 既节省内存,又方便统一管理

这就是原型存在的意义:让对象学会“蹭”!


🔗 二、四大核心概念:别再混淆 prototype 和 proto 了!

刚开始我总分不清 prototype__proto__,后来我画了张图,终于懂了。


1️⃣ 构造函数:创建实例的“模板”

function Student(name) {
  this.name = name;
}

它就是一个普通函数,但通常:

  • 首字母大写
  • new 调用

new 的过程可以简化为:

  1. 创建空对象 {};
  2. this 指向它;
  3. 执行函数体;
  4. 返回这个对象。

2️⃣ prototype:构造函数的“共享仓库”

每个函数都有一个 prototype 属性,它是一个对象,用来存放所有实例共享的内容

Student.prototype.species = '人类';
Student.prototype.study = function() {
  console.log(`${this.name}在努力学习`);
};

⚠️ 注意:prototype函数才有的属性


3️⃣ __proto__:实例通往原型的“梯子”

每个对象(包括实例)都有一个 __proto__ 属性(非标准但广泛支持),它指向其构造函数的 prototype

const s1 = new Student('张三');

console.log(s1.__proto__ === Student.prototype); // true

👉 这就是实例能访问到 sayHello 的原因:
s1.sayHello() → 自己没有 → 顺着 __proto__ 找 → 找到 Student.prototype.sayHello

🎯 记住一句话:实例的 __proto__ 指向构造函数的 prototype


4️⃣ constructor:原型的“回老家按钮”

原型对象上有一个 constructor 属性,指向构造函数本身。

console.log(Student.prototype.constructor === Student); // true
console.log(s1.constructor === Student); // true

⚠️ 重要提醒:手动重写 prototype 要修复 constructor!

Student.prototype = {
  sayHello() { console.log('hi') }
};

const s1 = new Student('张三');
console.log(s1.constructor === Student); // false ❌
console.log(s1.constructor === Object); // true → 错了!

// ✅ 修复:
Student.prototype = {
  constructor: Student,
  sayHello() { console.log('hi') }
};

否则后续 instanceof 判断可能出错。


📊 核心关系图(建议收藏)

lQLPJwvG0tJ1vuPNASLNAkSwAikK2ZgDD2YJAF6EAku5AA_580_290.png

📌 再说一遍:实例的 __proto__ 指向构造函数的 prototype,原型的 constructor 指向构造函数

🔍 三、原型查找机制:JS是怎么找到方法的?

当你调用 s1.sayHello() 时,JS 引擎是这样找的:

  1. 先看 s1 自己有没有 sayHello
  2. 没有?那就通过 __proto__Student.prototype 找;
  3. 还没有?继续通过 Student.prototype.__proto__ 找上一级;
  4. 直到找到,或者查到 null

这个链条,就是原型链

🖼️JavaScript 原型链完整关系图

66b94b61f939741c0ca1db2e69984697.png

1. 查找示例

function Student(name) {
  this.name = name;
}

Student.prototype.species = '人类';
Student.prototype.study = function() {
  console.log(`${this.name}在学习`);
};

const s1 = new Student('张三');

console.log(s1.name);        // 张三 → 自身属性
console.log(s1.species);     // 人类 → 来自 prototype
console.log(s1.toString());  // [object Object] → 来自 Object.prototype
console.log(s1.abc);         // undefined → 找不到

2. 原型链终点:null

console.log(Object.prototype.__proto__); // null → 终点!

// 验证整个链:
console.log(s1.__proto__);                 // Student.prototype
console.log(s1.__proto__.__proto__);       // Object.prototype
console.log(s1.__proto__.__proto__.__proto__); // null

3. 实例属性可以“屏蔽”原型属性

function Student(name) {
  this.name = name;
  this.species = '外星人'; // 覆盖原型属性
}

Student.prototype.species = '人类';

const s1 = new Student('张三');
console.log(s1.species); // 外星人

delete s1.species;
console.log(s1.species); // 人类 → 删除后重新查找原型

✅ 应用:为个别实例定制行为,不影响全局。


🧬 四、原型式继承:JS的“继承”到底是什么?

传统语言是“类继承”(血缘关系),而 JS 是“委托继承”——你不会,就去问你爸,你爸不会,就去问爷爷。

1. 经典继承实现

// 父类
function Person(name) {
  this.name = name;
}
Person.prototype.greet = function() {
  console.log(`你好,我是${this.name}`);
};

// 子类
function Student(name, grade) {
  Person.call(this, name); // 继承父类实例属性
  this.grade = grade;
}

// 继承父类原型方法
Student.prototype = new Person();
Student.prototype.constructor = Student;

// 扩展子类方法
Student.prototype.study = function() {
  console.log(`${this.name}在读${this.grade}年级`);
};

const s1 = new Student('张三', 3);
s1.greet(); // 你好,我是张三(继承)
s1.study(); // 张三在读3年级(自有)

2. ES6 class 只是语法糖

class Person {
  constructor(name) { this.name = name; }
  greet() { console.log(`你好,我是${this.name}`); }
}

class Student extends Person {
  constructor(name, grade) {
    super(name);
    this.grade = grade;
  }
  study() { console.log(`${this.name}在读${this.grade}年级`); }
}

底层依然是原型链驱动。class 不是新东西,只是让你写得更爽。


💡 五、原型的实际应用

1. 工具类共享方法

function Utils() {}
Utils.prototype.formatDate = function(date) { /* ... */ };

2. 扩展原生对象(谨慎!)

Array.prototype.unique = function() {
  return [...new Set(this)];
};
[1,2,2,3].unique(); // [1,2,3]

⚠️ 注意:生产环境慎用,避免污染全局。

3. 单例模式

function Singleton() {
  if (Singleton.prototype.instance) {
    return Singleton.prototype.instance;
  }
  this.data = '唯一实例';
  Singleton.prototype.instance = this;
}

4. 框架中的应用(如 Vue)

Vue.prototype.$http = axios; // 所有组件都能用 this.$http

⚠️ 六、常见误区

❌ 误区1:混淆 prototype 和 proto

  • prototype:函数才有,是“仓库”
  • __proto__:对象都有,是“梯子”

❌ 误区2:覆盖 prototype 不修 constructor

会导致 instanceof 失效。

✅ 正确做法:永远记得修 constructor!


🏁 七、总结:原型是JS的灵魂

核心要点 说明
🔹 核心价值 共享方法,节省内存
🔹 核心关系 实例.__proto__ === 构造函数.prototype
🔹 查找机制 自身 → 原型链 → null
🔹 继承本质 委托查找,非类继承
🔹 class 本质 原型的语法糖

🌟 最后感悟
学原型的过程,就像在迷宫中找出口。
一开始觉得混乱,但当你画出那张关系图,执行第一段可运行代码,听到“啊哈!”的那一声——
你就真正理解了 JavaScript 的灵魂。

Taro 小程序页面返回传参完整示例

2025年11月29日 16:00

前言

  • 我们在开发的时候,有时候会遇到,A页面跳转到B页面,B页面改一些数据(例如:收藏状态),回到A页面的时候不想刷新A页面,并看到最新的数据状态;
  • 对于以上场景,有以下几种解决方案;

方法一:EventChannel(推荐)

PageA.jsx - 跳转页面

import React, { useState } from 'react'
import { View, Button, Text } from '@tarojs/components'
import Taro from '@tarojs/taro'

const PageA = () => {
  const [receivedData, setReceivedData] = useState(null)

  const goToPageB = () => {
    Taro.navigateTo({
      url: '/pages/pageB/index',
      events: {
        // 监听返回数据
        // ⚠️ 这里监听的事件名 必须和 子页面绑定的事件名称相同
        onReturnData: (data) => {
          console.log('接收到返回数据:', data)
          setReceivedData(data)
        },
        // 可以监听多个事件
        onSelectItem: (item) => {
          console.log('选中的项目:', item)
        }
      }
    })
  }

  return (
    <View className="page-a">
      <Button onClick={goToPageB}>跳转到页面B</Button>
      
      {receivedData && (
        <View className="received-data">
          <Text>接收到的数据:</Text>
          <Text>{JSON.stringify(receivedData)}</Text>
        </View>
      )}
    </View>
  )
}

export default PageA

PageB.jsx - 返回页面

  • 这是
import React, { useState, useEffect } from 'react'
import { View, Button, Input } from '@tarojs/components'
import Taro from '@tarojs/taro'

const PageB = () => {
  // 可以使用 useState 或 useRef 存储 EventChannel
  const [eventChannel, setEventChannel] = useState(null)
  const [inputValue, setInputValue] = useState('')
  const [count, setCount] = useState(0)

  useEffect(() => {
    // 获取 EventChannel
    const channel = Taro.getCurrentInstance().page?.getOpenerEventChannel?.()
    if (channel) {
      setEventChannel(channel)
    }
  }, [])

  const handleReturn = () => {
    if (eventChannel) {
      // 发送数据给上个页面
      eventChannel.emit('onReturnData', {
        inputValue,
        timestamp: Date.now(),
        source: 'pageB'
      })
    }
    
    // 返回上个页面
    Taro.navigateBack()
  }

  const handleSelectItem = (item) => {
    if (eventChannel) {
      eventChannel.emit('onSelectItem', item)
    }
  }
  
  // ---- 页面销毁传递参数 Start ----
  // 若是使用小程序的导航栏的返回按钮,可以在页面销毁的时候,向父页面传递参数
  // 需要注意的是,useUnload 的参数若是依赖于一些数据,需要使用 useCallback 对函数进行缓存
  const handleBack = useCallback(() => {
    if (eventChannel) {
      eventChannel?.emit('onReturnPageA', { name: 'PageA', count })
      console.log('数据发送成功')
    }
  }, [eventChannel, count])

  useUnload(handleBack)
  // ---- 页面销毁传递参数 End ----

  return (
    <View className="page-b">
      <Input
        value={inputValue}
        onInput={(e) => setInputValue(e.detail.value)}
        placeholder="输入要传递的数据"
      />
      
      <Button onClick={handleReturn}>返回并传递数据</Button>
      
      <Button onClick={() => handleSelectItem({ id: 1, name: '选项1' })}>
        选择选项1
      </Button>
      
      <Button onClick={() => setCount(v => v++)}>
        改变count
      </Button>
      
      <Button onClick={() => handleSelectItem({ id: 2, name: '选项2' })}>
        选择选项2
      </Button>
    </View>
  )
}

export default PageB

方法二:使用 Zustand 状态管理

store/index.js

import { create } from 'zustand'

const useAppStore = create((set, get) => ({
  // 页面返回数据
  pageReturnData: null,
  
  // 设置返回数据
  setPageReturnData: (data) => set({ pageReturnData: data }),
  
  // 清除返回数据
  clearPageReturnData: () => set({ pageReturnData: null }),
  
  // 获取并清除返回数据
  getAndClearReturnData: () => {
    const data = get().pageReturnData
    set({ pageReturnData: null })
    return data
  }
}))

export default useAppStore

PageA.jsx - 使用状态管理

import React, { useEffect } from 'react'
import { View, Button, Text } from '@tarojs/components'
import Taro, { useDidShow } from '@tarojs/taro'
import useAppStore from '../store'

const PageA = () => {
  const { pageReturnData, clearPageReturnData } = useAppStore()

  // 页面显示时检查返回数据
  useDidShow(() => {
    if (pageReturnData) {
      console.log('接收到返回数据:', pageReturnData)
      // 处理数据后清除
      handleReturnData(pageReturnData)
      clearPageReturnData()
    }
  })

  const handleReturnData = (data) => {
    // 处理返回的数据
    Taro.showToast({
      title: `接收到: ${data.message}`,
      icon: 'success'
    })
  }

  const goToPageB = () => {
    Taro.navigateTo({
      url: '/pages/pageB/index'
    })
  }

  return (
    <View className="page-a">
      <Button onClick={goToPageB}>跳转到页面B</Button>
    </View>
  )
}

export default PageA

PageB.jsx - 设置状态并返回

import React, { useState } from 'react'
import { View, Button, Input } from '@tarojs/components'
import Taro from '@tarojs/taro'
import useAppStore from '../store'

const PageB = () => {
  const [message, setMessage] = useState('')
  const setPageReturnData = useAppStore(state => state.setPageReturnData)

  const handleReturn = () => {
    // 设置要传递的数据
    setPageReturnData({
      message,
      timestamp: Date.now(),
      type: 'user_input'
    })
    
    // 返回上个页面
    Taro.navigateBack()
  }

  return (
    <View className="page-b">
      <Input
        value={message}
        onInput={(e) => setMessage(e.detail.value)}
        placeholder="输入消息"
      />
      
      <Button onClick={handleReturn}>返回并传递消息</Button>
    </View>
  )
}

export default PageB

方法三:自定义 Hook 封装

hooks/usePageReturn.js

import { useState, useEffect } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'

// 全局存储返回数据
let globalReturnData = new Map()

export const usePageReturn = (pageKey) => {
  const [returnData, setReturnData] = useState(null)

  useDidShow(() => {
    const data = globalReturnData.get(pageKey)
    if (data) {
      setReturnData(data)
      globalReturnData.delete(pageKey)
    }
  })

  const setReturnDataForPage = (targetPageKey, data) => {
    globalReturnData.set(targetPageKey, data)
  }

  const clearReturnData = () => {
    setReturnData(null)
  }

  return {
    returnData,
    setReturnDataForPage,
    clearReturnData
  }
}

// 导航并设置返回监听
export const navigateToWithReturn = (url, pageKey, onReturn) => {
  return Taro.navigateTo({
    url,
    events: {
      returnData: (data) => {
        if (onReturn) {
          onReturn(data)
        }
      }
    }
  })
}

使用自定义 Hook

// PageA.jsx
import React from 'react'
import { View, Button } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { usePageReturn } from '../hooks/usePageReturn'

const PageA = () => {
  const { returnData, clearReturnData } = usePageReturn('pageA')

  useEffect(() => {
    if (returnData) {
      console.log('接收到返回数据:', returnData)
      // 处理数据
      clearReturnData()
    }
  }, [returnData])

  const goToPageB = () => {
    Taro.navigateTo({
      url: '/pages/pageB/index?fromPage=pageA'
    })
  }

  return (
    <View>
      <Button onClick={goToPageB}>跳转到页面B</Button>
    </View>
  )
}

export default PageA

最佳实践建议

  1. 简单场景:使用 EventChannel(方法一)
  2. 复杂应用:使用状态管理(方法二)
  3. 多页面复用:封装自定义 Hook(方法三)
  4. 数据量大:避免使用 URL 参数,选择状态管理
  5. 临时数据:使用 EventChannel,自动清理
  6. 持久数据:结合本地存储使用

注意事项

  • EventChannel 只在 navigateTo 时可用,redirectTo 不支持
  • 状态管理要注意及时清理数据,避免内存泄漏
  • 复杂对象传递时注意序列化问题
  • 考虑页面栈的层级关系,避免数据传递错乱

TypeScript的泛型工具集合

作者 ErMao
2025年11月29日 15:52

TypeScript的泛型工具集合

TypeScript中集合了很多泛型工具,在日常开发中,我们经常会看到这类工具的使用,所以属性这类工具也是必备的。

工具集大概能够分为几类:

  • 对象与属性工具
  • 联合类型工具
  • 函数工具
  • 类工具
  • 字符串工具
  • this工具
  • promise工具

对象与属性工具
Partial<T>

将类型的所有属性设置为可选属性

interface User {
  id: number;
  name: string;
  age: number;
}
const user2: Partial<User> = { id: 2, name: "ErMao" };
Required<T>

将类型的所有属性设置为必填

interface Config {
  port?: number;
  host?: string;
}
const c1: Config = { port: 8080 };
const c2: Required<Config> = { port: 8080, host: "localhost" };
Readonly<T>

将类型的所有属性设置为只读

interface Todo {
  title: string;
  done: boolean;
}
const t: Readonly<Todo> = { title: "Clean", done: false };
// t.done = true // 错误:不能分配到 "done" ,因为它是只读属性
Record<K,T>

用联合类型键映射到统一的值类型。这个工具很特别,可以把类型作为对象的键。

type Status = "success" | "error" | "loading";

const statusMap: Record<Status, string> = {
  success: "成功",
  error: "错误",
  loading: "加载中",
};
Pick<T, K>

从类型 T 中选择一组属性 K

interface User {
  id: number;
  name: string;
  age: number;
}

const u3: Pick<User, "id" | "name"> = { id: 2, name: "ErMao" };
Omit<T, K>

从类型 T 中排除一组属性 K

interface TodoList {
  title: string;
  description: string;
  completed: boolean;
  createdAt: number;
}
type TodoWithoutMeta = Omit<TodoList, "createdAt" | "completed">;
const x: TodoWithoutMeta = { title: "Clean", description: "Room" };

从这些方法中,可以从对象的键数量和属性进行记忆:

多 >>> 少:Partial、Pice 、Omit

少 >>> 多:Required 、Record

属性:Readonly

联合类型工具
Exclude<T, U>

从类型 T 中排除 U 类型的成员

// Exclude<T, U> : 从类型 T 中排除 U 类型的成员
type Status2 = "pending" | "success" | "error";
type NonError = Exclude<Status2, "error">;
NonNullable<T>

从类型 T 中排除 null 和 undefined 类型的成员,和 Exclude 类似。

type MaybeString = string | null | undefined;
type StrictString = NonNullable<MaybeString>;
Extract<T, U>

从类型 T 中提取 U 类型的成员。类似于交集,但是与&交叉类型又有不同。

type S1 = "a" | "b" | "c";
type S2 = "b" | "d";
type O1 = {name: string}
type O2 = {age: number}
type In2 = O1 & O2
const in2: In2 = {name: "ErMao", age: 18}
type In = S1 & S2;
type Intersection = Extract<S1, S2>;
type In3 = Extract<O1 , O2> // never

排除 : Exclude、NonNullable
交集 : Extract

函数工具
Parameters<T>

从函数类型 T 中提取参数类型的元组

function fn(a: number, b: string) {}
type Args = Parameters<typeof fn>;
const valid: Args = [123, "hi"];
ReturnType<T>

获取函数返回类型

function makePoint() {
  return { x: 0, y: 0 };
}
type Point = ReturnType<typeof makePoint>;
const p: Point = { x: 1, y: 2 };
this 工具
ThisParameterType<T>

提取函数显式 this 参数的类型

interface Person {
  name: string;
}
function say(this: Person, msg: string) {
  return `${this.name}: ${msg}`;
}
type ThisT = ThisParameterType<typeof say>;

OmitThisParameter<T>

移除函数显式 this 参数

interface Person {
  name: string;
}
function say2(this: Person, msg: string) {
  return `${this.name}: ${msg}`;
}
const boundSay = say.bind({ name: "Ann" });
type FnNoThis = OmitThisParameter<typeof say2>;
const f: FnNoThis = boundSay;
ThisType<T>

为对象字面量中的 this 指定类型

type ObjectDescriptor<D, M> = {
  data: D;
  methods: M & ThisType<D & M>
};
function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
  return Object.assign({}, desc.data, desc.methods);
}
const obj = makeObject({
  data: { x: 0, y: 0 },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx;
      this.y += dy;
    },
  },
});
obj.moveBy(2, 3);
类工具
ConstructorParameters<T>

获取构造函数参数的元组类型

class Box {
  constructor(width: number, height?: number) {}
}
type CtorArgs = ConstructorParameters<typeof Box>;
const args: CtorArgs = [100, 50];
InstanceType<T>

获取构造函数实例的类型

class UserService {
  constructor(public name: string) {}
  greet() {
    return `Hi ${this.name}`;
  }
}
type ServiceInstance = InstanceType<typeof UserService>;
const svc: ServiceInstance = new UserService("Leo");

字符串工具
Uppercase<S>

转成全大写

type U = Uppercase<"hello world">
Lowercase<S>

转成全小写

type L = Lowercase<"HELLO WORLD">
Capitalize<S>

将字符串的第一个字符转换为大写

type Capitalized = Capitalize<"hello world">
Uncapitalize<S>

将字符串的第一个字符转换为小写

type Uncapitalized = Uncapitalize<"Hello World">

Promise 工具
Awaited<T>
type A = Awaited<Promise<string>>
type B = Awaited<Promise<Promise<number>>>
type C = Awaited<boolean | Promise<number>>
async function fetchNum() { return 42 }
type R = Awaited<ReturnType<typeof fetchNum>>

element-plus源码解读1——useNamespace

作者 Joie
2025年11月29日 15:50

useNamespace

useNamespace位于packages/hooks/use-namespace, 旨在帮所有组件统一生成类名/变量名,遵循BEM规范

什么是BEM规范?可阅读下面这篇文章blog.csdn.net/fageaaa/art…

element-plus的BEM类名生成函数_bem

const _bem = (
  namespace: string, // 命名空间,通常是el
  block: string, // 块名,例如button
  blockSuffix: string, // 块后缀(可选),用于块的变体
  element: string, // 元素(可选),用__连接
  modifier: string // 修饰符(可选),用--连接
) => {
  let cls = `${namespace}-${block}`
  if (blockSuffix) {
    cls += `-${blockSuffix}`
  }
  if (element) {
    cls += `__${element}`
  }
  if (modifier) {
    cls += `--${modifier}`
  }
  return cls
}

### 1. 参数说明

-   namespace:命名空间,通常是 'el'
-   block:块名,如 'button'
-   blockSuffix:块后缀(可选),用于块的变体
-   element:元素(可选),用 __ 连接
-   modifier:修饰符(可选),用 -- 连接

### 2. 生成规则(按顺序拼接)

-   基础:namespace-block → 'el-button'
-   如果有 blockSuffix:追加 -${blockSuffix} → 'el-button-suffix'
-   如果有 element:追加 __${element} → 'el-button__icon'
-   如果有 modifier:追加 --${modifier} → 'el-button--primary'

el-button组件为例子

const ns = useNamespace('button')

ns.namespace.value  // → 'el'
  • b-Block(块)
const b = (blockSuffix = '') => _bem(namespace.value, block, blockSuffix, '', '')

ns.b()  // el-button
ns.b('group')  // el-button-group
  • e-Element(元素)
const e = (element?: string) => element ? _bem(namespace.value, block, '', element, '') : ''

ns.e('icon')  // el-button__icon
ns.e('text')  // el-button__text
ns.e()  // 返回一个空字符串'', 因为传入的element:string参数是空
  • e-Modifier(修饰符)
const m = (modifier?: string) => modifier ? _bem(namespace.value, block, '', '', modifier) : ''

ns.m('primary')  // el-button--primary
ns.m('small')  // el-button--small
ns.m('disabled')  // el-button--disabled
ns.m()  // '' (空字符串)
  • be-Block+Element (块后缀+元素)
  const be = (blockSuffix?: string, element?: string) =>
    blockSuffix && element
      ? _bem(namespace.value, block, blockSuffix, element, '')
      : ''

ns.be('group', 'item') // el-button-group__item
ns.be('group', '') // ''
ns.be('', 'group')  // ''
  • em-Element+Modifier (元素+修饰符)
  const em = (element?: string, modifier?: string) =>
    element && modifier
      ? _bem(namespace.value, block, '', element, modifier)
      : ''
      
ns.em('icon', 'loading') // el-button__icon--loading
ns.em('text', 'expand') // el-button__text--expand
ns.em('icon', '') // ''
ns.em('', 'loading') // ''
  • bm-Block+Modifier (块后缀+修饰符)
  const bm = (blockSuffix?: string, modifier?: string) =>
    blockSuffix && modifier
      ? _bem(namespace.value, block, blockSuffix, '', modifier)
      : ''
      
ns.bm('group', 'vertical') // el-button-group--vertical
ns.bm('group', '') // ''
ns.bm('', 'primary') // ''
  • bem-Block+Element+Modifier (块后缀+元素+修饰符)
  const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
    blockSuffix && element && modifier
      ? _bem(namespace.value, block, blockSuffix, element, modifier)
      : ''
      
ns.bem('group', 'item', 'active') // el-button-group__item--active
ns.bem('group', 'item', '') // '' 必须三个参数都有值
  • is-State 状态类
  const statePrefix = 'is-'

  const is: {
    (name: string, state: boolean | undefined): string
    (name: string): string
  } = (name: string, ...args: [boolean | undefined] | []) => {
    const state = args.length >= 1 ? args[0]! : true // args[0]! ts的非空断言
    return name && state ? `${statePrefix}${name}` : ''
  }
  
ns.is('loading')  // is-loading
ns.is('loading', true)  // is-loading
ns.is('loading', false)  // ''
ns.is('disabled', true)  // is-disabled
ns.is('disabled', undefined) // ''
  • cssVar-CSS变量(全局命名空间)
  const cssVar = (object: Record<string, string>) => {
    const styles: Record<string, string> = {}
    for (const key in object) {
      if (object[key]) {
        styles[`--${namespace.value}-${key}`] = object[key]
      }
    }
    return styles
  }
  
  ns.cssVar({ color: 'red', size: '10px'}) // {'--el-color': 'red', '--el-size': '10px'}
  • cssVarName-CSS 变量名(全局)
const cssVarName = (name: string) => `--${namespace.value}-${name}`

ns.cssVarName('color')  // → '--el-color'
ns.cssVarName('size')   // → '--el-size'

补充:命名空间与变量名的区别 命名空间:用{}包裹起来的批量的CSS变量+赋值,可以直接绑定到元素的style属性上 变量名:仅仅是一个单独的没有被赋值的变量,需要自己使用

cssVar 的使用场景(批量设置变量值)

<template>
  <div :style="customStyles">
    <!-- 这个 div 会应用这些 CSS 变量 -->
  </div>
</template>

<script setup>
const ns = useNamespace('button')
const customStyles = ns.cssVar({
  color: 'blue',
  fontSize: '16px'
})
// customStyles = { '--el-color': 'blue', '--el-fontSize': '16px' }
</script>

cssVarName 的使用场景(引用已存在的变量)

<template>
  <div :style="{ color: `var(${colorVarName})` }">
    <!-- 使用 cssVarName 获取变量名,然后用 var() 引用 -->
  </div>
</template>

<script setup>
const ns = useNamespace('button')
const colorVarName = ns.cssVarName('color')
// colorVarName = '--el-color'

// 然后在 CSS 或 style 中使用:
// color: var(--el-color)
</script>
  • cssVarBlock-CSS变量(带block)
  const cssVarBlock = (object: Record<string, string>) => {
    const styles: Record<string, string> = {}
    for (const key in object) {
      if (object[key]) {
        styles[`--${namespace.value}-${block}-${key}`] = object[key]
      }
    }
    return styles
  }
  
============
// 步骤 1: 创建命名空间实例,传入 'button' 作为 block
const ns = useNamespace('button')
// 此时 ns 内部保存了 block = 'button'

// 步骤 2: 调用 cssVarBlock
ns.cssVarBlock({ color: 'blue', fontSize: '14px' })

// 步骤 3: cssVarBlock 内部使用闭包中的 block
// 生成:'--el-button-color': 'blue'
// 生成:'--el-button-fontSize': '14px'
===========
ns.cssVarBlock({ color: 'blue', fontSize: '14px' })
// → { '--el-button-color': 'blue', '--el-button-fontSize': '14px' }
  • cssVarBlockName-CSS变量名(带block)
  const cssVarBlockName = (name: string) =>
    `--${namespace.value}-${block}-${name}`

ns.cssVarBlockName('color') // --el-button-color
ns.cssVarBlockName('bgColor') // --el-button-bgColor
❌
❌