Joyber 发布的文章

前端开发中,引入美观的中文字体(如阿里巴巴普惠体)往往会遇到一个痛点:完整中文字体文件体积巨大(通常 5MB~10MB),直接引入会严重拖慢页面加载速度。本文将介绍如何通过 Font Splitter 工具结合 CSS unicode-range 特性,实现字体文件的按需加载,大幅优化页面性能。


一、问题背景:大字体文件的性能困境

当我们在项目中全局引入中文字体时:

body {
  font-family: 'alibaba-pu-hui-ti-3-regular', Arial, Helvetica, sans-serif;
}

如果直接引入完整字体文件(如 8.5MB 的阿里巴巴普惠体),会带来以下问题:

  • 首屏加载慢:用户需要等待完整字体下载完成才能看到渲染后的文字,影响体验。
  • 带宽浪费:页面实际只用到几百个常用字符,却要下载包含数万字符的完整字体。
  • 缓存冗余:即使只修改了几个字,也需要重新下载整个大文件。

二、核心原理:unicode-range 按需加载

CSS 的 @font-face 规则提供了 unicode-range 属性,它允许我们为同一个字体族定义多个子字体文件,并指定每个子文件负责渲染的 Unicode 字符范围。

浏览器的工作流程如下:

  1. 扫描页面文字:解析页面时,提取所有可见文字的 Unicode 编码。
  2. 匹配字符范围:将文字编码与 @font-face 中的 unicode-range 逐一比对。
  3. 按需下载:只下载包含页面所需字符的子字体文件,而非完整字体。
  4. 渲染文字:用下载的子字体文件渲染对应字符,未覆盖的字符自动使用兜底字体。

三、工具选择:Font Splitter 快速分割字体

Font Splitter 是一个轻量级命令行工具,能自动将大字体文件按 Unicode 范围分割成多个小文件,并生成对应的 CSS 代码,完美适配 unicode-range 按需加载方案。

1. 安装 Font Splitter

npm install -g @vdustr/font-splitter

若生成 woff2 格式时提示 Brotli 缺失,需先安装 Python 依赖:

pip install Brotli

2. 基础使用命令

# 基础用法:分割字体并生成 woff2 格式
font-splitter ./AlibabaPuHuiTi-3-55-Regular.ttf -f woff2

# 自定义字体族名称(与项目中 font-family 保持一致)
font-splitter ./AlibabaPuHuiTi-3-55-Regular.ttf -f woff2 -n alibaba-pu-hui-ti-3-regular

# 不生成 local() 声明(避免中文乱码)
font-splitter ./AlibabaPuHuiTi-3-55-Regular.ttf -f woff2 -n alibaba-pu-hui-ti-3-regular --no-local

3. 输出结果

执行命令后,会在当前目录生成 output 文件夹,包含:

  • 多个分割后的字体文件(如 AlibabaPuHuiTi-3-55-Regular.Basic-Latin.woff2
  • 对应的 CSS 文件(如 AlibabaPuHuiTi-3-55-Regular.css

四、项目集成:无缝接入全局字体

1. 资源放置

将分割后的字体文件和 CSS 文件放入项目静态资源目录,例如:

├── public/
│   └── fonts/
│       ├── AlibabaPuHuiTi-3-55-Regular.Basic-Latin.woff2
│       ├── AlibabaPuHuiTi-3-55-Regular.Latin-1-Supplement.woff2
│       └── ...
└── src/
    └── assets/
        └── css/
            └── AlibabaPuHuiTi-3-55-Regular.css

2. 修改 CSS 路径

打开生成的 CSS 文件,将 url() 中的字体路径修改为项目实际路径:

@font-face {
  font-family: alibaba-pu-hui-ti-3-regular;
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/AlibabaPuHuiTi-3-55-Regular.Basic-Latin.woff2') format('woff2');
  unicode-range: U+0, U+d, U+20-7e;
}
若生成的 CSS 中存在乱码的 local() 项,可直接删除,不影响功能。

3. 全局引入

在项目的全局样式入口(如 main.css)中引入分割后的 CSS:

/* 先引入分割字体的 CSS,定义字体族 */
@import './assets/css/AlibabaPuHuiTi-3-55-Regular.css';

/* 原有全局字体配置,无需修改 */
body {
  font-family: 'alibaba-pu-hui-ti-3-regular', Arial, Helvetica, sans-serif;
}

五、性能优化:结合 Nginx 缓存

为了进一步提升加载速度,我们可以对分割后的字体文件配置长期缓存:

location ~* \.(woff|woff2|ttf|otf|eot)$ {
    expires 30d;
    add_header Cache-Control "public, max-age=2592000, immutable";
    etag on;
    access_log off;
}
  • expires 30d:设置缓存有效期为 30 天。
  • Cache-Control: immutable:告诉浏览器该文件不会修改,避免重复验证请求。
  • etag on:生成文件唯一标识,文件更新时自动触发重新下载。

六、效果验证

1. 浏览器 Network 面板验证

打开浏览器开发者工具(F12)→ Network 面板,刷新页面后:

  • 观察字体文件请求:只会下载页面实际用到的字符范围对应的小文件。
  • 查看响应头:确认 Cache-Control 等缓存头已生效。

2. 加载速度对比

方案字体文件大小首屏加载时间带宽占用
完整字体引入~8.5MB
Font Splitter 分割后~2MB(常用中文+英文)

七、常见问题与解决方案

1. CSS 中出现乱码

原因:Font Splitter 生成 local() 时,将字体的中文名称直接写入 CSS,导致编码问题。
解决

  • 方案 1:删除乱码的 local() 项,只保留 url()
  • 方案 2:生成时添加 --no-local 参数,从根源避免生成 local() 声明。

2. 字体不生效

排查方向

  • 确认 font-family 名称在 CSS 和项目中完全一致。
  • 检查字体文件路径是否正确。
  • 确认浏览器支持 woff2 格式(现代浏览器均支持)。

3. 生僻字显示异常

原因:生僻字未被分割后的 unicode-range 覆盖。
解决:重新运行 Font Splitter,调整分割范围,包含生僻字对应的 Unicode 区间。


八、总结

通过 Font Splitter + unicode-range 的组合,我们实现了:

  • 按需加载:只下载页面实际用到的字符,大幅减少字体体积。
  • 全局兼容:完美适配 body 全局字体配置,无需修改业务代码。
  • 性能最优:结合 Nginx 长期缓存,实现「一次下载,长久使用」。
  • 体验友好font-display: swap 确保文字先渲染,避免页面闪烁。

这种方案特别适合需要引入中文字体的前端项目,在保证视觉美观的同时,最大化提升页面加载性能。


九、延伸阅读


要理解 Git Stash 中「保持 Index(暂存区)」的作用,首先得先理清 Git 工作区的三个核心区域:

  • 工作区:你正在编辑的文件(未 git add)
  • 暂存区(Index):已经 git add 但未 git commit 的文件
  • 版本库:已经 git commit 的文件

「保持 Index」的核心作用

「保持 Index」对应的是 Git 命令行的 git stash save --keep-index(新版 Git 是 git stash push --keep-index),它的核心作用是:
执行 stash 时,只暂存「工作区中未加入暂存区的修改」,而保留暂存区(Index)的内容不变

通俗解释 + 场景举例

假设你有以下状态:

  1. 文件 A:修改后已 git add(在暂存区)
  2. 文件 B:修改后未 git add(仅在工作区)

情况 1:不勾选「保持 Index」(默认 stash)

执行 stash 后:

  • 暂存区(A)+ 工作区(B)的修改都会被暂存到 stash 中
  • 工作区 + 暂存区都会回到上一次 commit 的干净状态
  • 此时你既看不到 A 的修改,也看不到 B 的修改

情况 2:勾选「保持 Index」(--keep-index)

执行 stash 后:

  • 只有工作区的 B 会被暂存到 stash 中
  • 暂存区的 A 会被保留(依然在暂存区,工作区也保留 A 的修改)
  • 此时你仍能看到 A 的修改(暂存区+工作区都在),但 B 的修改被暂存走了

典型使用场景

这个功能最常用在「验证暂存区代码」的场景:
比如你已经把要提交的代码 git add 到暂存区,想先跑一遍测试/代码检查,确认这部分代码没问题,但又不想提交;同时工作区还有一些未完成的、不想被测试影响的修改。
此时勾选「保持 Index」:

  1. 把工作区的未完成修改 stash 走,让工作区变干净
  2. 保留暂存区的待提交代码,你可以放心跑测试/检查
  3. 测试通过后,直接 git commit 提交暂存区代码即可
  4. 之后再用 git stash pop 恢复之前暂存的未完成修改

PhpStorm 中的对应逻辑

在 PhpStorm 的 Git Stash 弹窗中:

  • 勾选「Keep Index」→ 对应 git stash push --keep-index
  • 不勾选 → 对应默认的 git stash push(暂存所有修改:工作区+暂存区)

总结

  1. 「保持 Index」的核心是保留暂存区的修改,只暂存工作区未 add 的内容
  2. 主要用于「验证暂存区代码」的场景,避免暂存区内容被 stash 清空;
  3. 对比默认 stash:默认会清空暂存区+工作区,而「保持 Index」只清空工作区未 add 的部分。

你想知道 PHP 的 uniqid() 函数生成的 ID 是否会重复,以及在什么场景下会重复、如何避免,对吧?

一、核心结论

uniqid() 有可能重复,并非绝对唯一,尤其是高并发/短时间内频繁调用的场景,重复概率会显著上升。

二、为什么会重复?先搞懂 uniqid() 的生成逻辑

uniqid() 的默认生成规则:

ID = 微秒级时间戳的16进制表示(默认13位) + 可选的随机数后缀

核心依赖的是服务器的系统时钟可选的随机数,这两个点都是重复的风险源:

1. 核心重复场景

场景重复原因重复概率
高并发调用同一微秒内多次调用 uniqid()(默认无后缀),系统时钟精度不足(比如Windows仅15ms精度),导致生成相同ID高(并发>1000/秒时极易出现)
分布式部署多台服务器的系统时钟同步误差,或同一时刻调用,可能生成相同ID
无随机数后缀仅依赖时间戳,短时间内调用会生成连续ID,极端情况(时钟回拨/精度低)会重复中高
服务器时钟回拨服务器时间被手动调整/同步,导致时间戳倒退,可能生成历史已存在的ID低但致命

2. 官方参数的影响(uniqid($prefix, $more_entropy)

  • $prefix:前缀,仅用于区分不同业务,不提升唯一性;
  • $more_entropy = true:在ID末尾追加随机数(约5位),能大幅降低重复概率,但仍无法完全杜绝。

三、验证:模拟重复场景(代码示例)

<?php
// 模拟高并发:同一微秒内调用1000次uniqid(无随机后缀)
$ids = [];
$repeatCount = 0;

// 循环调用,强制在同一微秒内执行(实际高并发会触发)
for ($i = 0; $i < 1000; $i++) {
    $id = uniqid(); // 默认无后缀
    if (in_array($id, $ids)) {
        $repeatCount++;
        echo "发现重复ID:{$id}\n";
    }
    $ids[] = $id;
}

echo "总调用次数:1000,重复次数:{$repeatCount}\n";

// 对比:开启more_entropy参数
$ids2 = [];
$repeatCount2 = 0;
for ($i = 0; $i < 1000; $i++) {
    $id = uniqid('', true); // 追加随机数后缀
    if (in_array($id, $ids2)) {
        $repeatCount2++;
        echo "开启随机后缀后重复ID:{$id}\n";
    }
    $ids2[] = $id;
}
echo "开启随机后缀后,总调用次数:1000,重复次数:{$repeatCount2}\n";

测试结果

  • 无后缀:高概率出现重复(尤其是Windows/低精度时钟服务器);
  • 有后缀:重复概率大幅降低,但极端并发下仍可能重复。

四、如何避免 uniqid() 重复?(按推荐度排序)

1. 推荐方案:组合多维度唯一标识

/**
 * 生成高可靠唯一ID(基于uniqid,避免重复)
 * @return string
 */
function generateUniqueId()
{
    // 组合:前缀 + 微秒时间戳 + 随机数 + 进程ID + 唯一后缀
    $prefix = 'ID';
    $microtime = substr(microtime(true) * 1000000, -6); // 6位微秒
    $random = mt_rand(100000, 999999); // 6位随机数
    $pid = getmypid(); // 进程ID(分布式可加服务器IP/ID)
    $uniq = uniqid('', true); // 带随机后缀的uniqid

    return $prefix . $microtime . $random . $pid . $uniq;
}

2. 进阶方案:使用PHP内置的加密级随机数

// 生成16位唯一ID(加密级随机,重复概率接近0)
$uniqueId = bin2hex(random_bytes(8)); // 8字节=16位16进制,密码学安全

3. 分布式场景:雪花算法(Snowflake)

如果是多服务器/分布式系统,优先用雪花算法(生成64位整数ID),包含:

  • 时间戳 + 机器ID + 进程ID + 序列号
    完全杜绝重复,Yii/Laravel等框架都有现成扩展(如 yii2-snowflake)。

五、uniqid() 的适用场景

场景是否推荐用 uniqid()替代方案
低并发(<100/秒)、非核心业务(如临时文件名)推荐(加more_entropy)-
高并发、核心业务(订单号/团购编号)不推荐雪花算法/组合唯一标识
分布式系统不推荐雪花算法/UUIDv4

总结

  1. uniqid() 不是绝对唯一:默认仅依赖微秒时间戳,高并发/时钟精度低时极易重复;
  2. 开启 $more_entropy = true 可大幅降低重复概率,但无法完全避免;
  3. 核心业务/高并发场景:放弃纯 uniqid(),改用「时间戳+随机数+进程/机器ID」组合,或雪花算法/UUIDv4;
  4. 低并发/非核心场景:uniqid('', true) 可满足基本需求。

抖音优惠券核销对接自有平台优惠券,核心是通过抖音开放平台接口完成券码核验、状态同步,再联动自有平台券的发放/核销,关键在于权限申请、门店与券码映射、接口调用及异常处理。以下是完整对接方案与实操步骤。


一、核心对接模式与前提准备

准备项说明
账号与权限1. 注册抖音开放平台开发者账号,创建应用(小程序/服务端应用);2. 申请团购核销、优惠券管理等权限(如promotion.coupon.admin、life.capacity.fulfilment);3. 完成商家/门店授权,绑定抖音门店ID与自有平台门店ID
券码体系映射1. 抖音券(团购券/三方券)与自有平台券建立批次关联(如活动ID绑定);2. 支持抖音券核销后自动发放自有平台券,或三方券由自有平台生成并同步抖音
接口基础1. 接入抖音验券、核销、取消核销等核心接口;2. 自有平台需提供券发放、核销、状态查询接口,支持幂等处理

二、完整对接流程(分两种场景)

场景1:核销抖音券后联动自有平台发券

  1. 用户下单与券码生成

    • 用户在抖音购买团购券,抖音生成券码(12位数字)或加密数据(encrypted_data),订单状态为待核销。
    • 自有平台通过抖音订单查询接口获取订单与券码信息,建立订单-券码映射。
  2. 核销流程

    1. 验券准备:调用抖音验券准备接口(/api/douyin-code/verify/preparation),传入券码(code)或加密数据(encrypted_data),获取可用券列表与加密券码。
    2. 抖音券核销:调用核销接口(/coupons/verifyV2),传入verify_token(UUID)、订单ID、券码等,返回核销结果(error_code=0为成功)。
    3. 自有平台联动:核销成功后,调用自有平台发券接口,给用户发放对应优惠券,并记录核销流水与关联关系。
  3. 状态同步与结算

    • 抖音侧:核销后T+2天自动推进订单完成(支持取消核销);自有平台同步更新券状态为“已核销”。
    • 结算:按抖音规则完成资金结算,自有平台同步记账。

场景2:自有平台生成三方券同步抖音核销

  1. 三方券发码

    • 自有平台生成券码,通过抖音三方券码订单发货接口同步券码至抖音,标记order_type=5(待发码订单)。
  2. 核销与状态同步

    • 直接调用抖音核销接口(/coupons/verifyV2)完成核销,无需验券准备;取消核销调用/coupons/cancelVerify。
    • 自有平台同步券状态,处理退款、过期退等逆向场景。

三、核心接口调用详解

接口名称接口地址作用关键参数调用注意
验券准备https://open.douyin.com/api/douyin-code/verify/preparation获取可用券列表code/encrypted_data、门店ID优先用encrypted_data,二维码变更会导致失效
核销接口https://open.douyin.com/api/douyin-code/verify核销抖音券/三方券verify_token、订单ID、券码verify_token重试时需保持一致,支持批量验券(不跨订单)
批量核销(小程序券)https://open.douyin.com/api/promotion/v1/coupon/batch_consume_coupon/核销小程序券coupon_id_list、open_id一次最多核销10张,需用户open_id
取消核销https://open.douyin.com/api/douyin-code/cancelVerify撤销核销verify_id、券码仅核销后T+2天内支持,用于异常修正

四、自有平台联动关键步骤

  1. 券发放触发

    • 监听抖音核销回调(配置核销通知地址),或主动轮询核销结果接口,获取核销成功事件。
    • 调用自有平台发券接口,按规则发放优惠券(如满减券、折扣券),记录抖音券ID与自有券ID的关联。
  2. 状态一致性保障

    • 双方券状态双向同步:抖音券核销/退款后,同步自有平台券状态;自有平台券核销后,若关联抖音券,需同步更新。
    • 实现幂等:通过verify_token、订单ID等唯一标识,避免重复核销/发券。
  3. 异常处理

    • 核销失败:重试机制(间隔递增),记录失败日志,人工介入处理。
    • 取消核销:抖音侧调用cancelVerify后,自有平台同步撤销对应券的发放或恢复状态。
    • 退款场景:抖音订单退款时,自有平台回收已发放的优惠券。

五、YII2框架下的对接示例(PHP)

以下是YII2中调用抖音核销接口并联动自有平台发券的简化代码:

// 1. 验券准备
public function actionVerifyPrep() {
    $code = Yii::$app->request->post('code');
    $client = new \GuzzleHttp\Client();
    $response = $client->post('https://open.douyin.com/api/douyin-code/verify/preparation', [
        'headers' => ['Authorization' => 'Bearer ' . $this->getAccessToken()],
        'json' => ['code' => $code, 'store_id' => 'your_store_id']
    ]);
    $result = json_decode($response->getBody(), true);
    if ($result['error_code'] == 0) {
        return $this->actionVerify($result['encrypted_coupon_code'], $result['order_id']);
    }
    return ['error' => '验券准备失败'];
}

// 2. 抖音券核销+自有平台发券
public function actionVerify($encryptedCode, $orderId) {
    $verifyToken = \Ramsey\Uuid\Uuid::uuid4()->toString();
    $client = new \GuzzleHttp\Client();
    $response = $client->post('https://open.douyin.com/coupons/verifyV2', [
        'headers' => ['Authorization' => 'Bearer ' . $this->getAccessToken()],
        'json' => [
            'verify_token' => $verifyToken,
            'order_id' => $orderId,
            'encrypted_coupon_code' => $encryptedCode
        ]
    ]);
    $result = json_decode($response->getBody(), true);
    if ($result['error_code'] == 0) {
        // 调用自有平台发券接口
        $this->sendOwnCoupon($orderId, Yii::$app->user->id);
        return ['success' => true];
    }
    return ['error' => '核销失败'];
}

// 3. 自有平台发券逻辑
private function sendOwnCoupon($orderId, $userId) {
    $coupon = new OwnCoupon();
    $coupon->user_id = $userId;
    $coupon->order_id = $orderId;
    $coupon->batch_id = 'tie_to_douyin_activity';
    $coupon->save();
}

六、关键注意事项

  1. 权限与安全

    • 接口调用需携带access_token,通过OAuth2.0获取并定期刷新。
    • 核销通知地址需配置白名单,防止非法请求。
  2. 逆向流程

    • 支持随时退、过期自动退,退款时同步撤销自有平台券的发放。
    • 取消核销需在T+2天内操作,超时无法撤销。
  3. 性能与监控

    • 接口调用添加超时重试(如3次),verify_token重试时保持一致。
    • 监控核销成功率、接口响应时间,设置告警机制。

七、交付物清单

  1. 抖音开放平台应用配置文档(权限、门店绑定、回调地址)。
  2. 券码映射关系表(抖音活动ID-自有平台券批次ID)。
  3. 接口调用手册(验券、核销、发券接口参数与示例)。
  4. 异常处理与重试机制代码片段。