普通视图

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

Rspack 加入 Next.js 生态:携手改进前端工具链

作者 WebInfra
2025年4月10日 15:21

Rspack 的核心目标之一,是提供卓越的开发体验,并且能够与基于 webpack 的项目实现无缝集成,将迁移成本降至最低。

在 JavaScript 生态中,Next.js 拥有深度定制的 webpack 配置和丰富的插件生态,这使它成为验证 Rspack 兼容性和健壮性的理想选择。通过将 Rspack 集成到 Next.js 中,不仅展示了 Rspack 在复杂项目中的适用性,还为 Next.js 用户提供了一个改善开发体验的替代方案。

Rspack 携手 Next.js

今天,我们很高兴地推出 next-rspack,这是一个社区驱动的插件,让 Next.js 能够直接使用 Rspack 作为打包工具。对于尚未准备好采用 Turbopack 的团队,这个插件提供了一种高性能、且与 webpack 兼容的替代方案。

访问我们的 文档 或查看官方 Next.js 示例 开始使用。

在开发 next-rspack 之前,我们通过创建 rsnext(一个 Next.js 的分支)探索了集成的可能性,开发出了解决方案的原型。这一早期分支帮助我们验证了可行性,发现了许多边界 case,也让我们意识到,虽然 Rspack 与 webpack 的高度兼容性为我们提供了良好的起点,但实现稳定的集成仍然需要大量的努力和协作。

与 Vercel 的合作

next-rspack 的推出只是我们与 Vercel 广泛合作的一个方面。这种合作关系不仅限于 Next.js 集成,双方团队还致力于共同改进基础技术,如 SWCLightning CSS —— 这些工具在 JavaScript 生态中被广泛采用。

我们共同改进这些核心组件,为开发者创造更好的使用体验、性能和可靠性。这些努力不仅使 Rspack 和 Next.js 受益,也有助于提升整个 JavaScript 生态,让所有参与者都能受益。

为了保证长期可靠,next-rspack 已经集成到 Next.js 的持续集成流程中,这有助于主动发现问题并保持兼容性。尽管仍处于实验阶段,它目前通过了约 96% 的集成测试,这使我们有信心正式发布这一插件。你可以通过 arewerspackyet 跟踪最新状态,也可以关注 我们的 Twitter 了解 next-rspack 的最新进展。

对于尚未准备好采用 Turbopack 的团队,next-rspack 提供了一个稳定、高性能的替代方案,具有出色的兼容性和简单的接入过程。

我们由衷感谢 Vercel 的深度合作,以及双方对改进开发者工具体验的共同承诺。我们将持续协作,完善这一插件,共同推动现代 JavaScript 开发的未来。

当前性能

App Router 用户

目前,使用 next-rspack 的 App Router 实现比 Turbopack 慢,甚至可能比 webpack 还慢。这主要是因为某些 JavaScript 插件 在 Rust-JavaScript 的跨语言通信中产生了较大的性能开销。

我们已经实验性地将这些插件移植到 Rust 中,这极大地提高了性能 —— 大致与 Turbopack 相当。此外,我们正在研究如何解决深度集成带来的长期维护挑战。

Page Router 用户

Page Router 的情况要乐观得多:

  • 开发模式:比 webpack 快 2 倍
  • 生产模式:比 webpack 快 1.5 倍

高度依赖 webpack 生态的项目在迁移时将更加容易。

我们已经定位出一些限制性能提升的瓶颈,包括显著的 Rust-JavaScript 通信开销、较慢的 输出文件追踪 实现,这些问题将在未来得到解决。随着这些预期内的改进,我们预见:

  • 开发环境中构建和 HMR 速度提升 5 倍
  • 生产构建速度提升 3 倍

常见问题

它将如何保持支持?

next-rspack 已集成至 Next.js 的 CI 流程中,这使我们能够及早发现潜在问题并确保高度兼容性。随着 Next.js 与 Rspack 的共同发展,相关支持将不断得到完善。Rspack 团队、Vercel 团队以及开源社区将通力合作,共同支持这一插件。

谁在维护它?

next-rspack 是一个社区插件,但其开发和集成依赖于 Rspack 团队和 Vercel 团队之间的密切合作,以确保持续的支持和进展。

这对 Turbopack 有影响吗?Vercel 是否采用了 Rspack?

Rspack 不会替代 Turbopack。它是为那些拥有大量 webpack 配置、且尚未准备好迁移到 Turbopack 的用户提供的替代解决方案。

已知的问题有哪些?

截至目前,next-rspack 通过了约 96% 的集成测试,进展可在 arewerspackyet 上查看。

  • 一些特殊情况和进阶功能可能仍需要临时解决方案或额外支持。即使你没有遇到问题,也欢迎在 反馈讨论 中告诉我们你的使用体验。
  • 受当前插件实现方式的限制,App Router 的性能表现不够理想,这方面仍有很大的改进空间。
  • 由于 Rspack 不是 100% 兼容 webpack 的 API,你的一些 webpack 插件可能无法在 Rspack 上顺畅工作,如果你遇到兼容性问题,欢迎向我们反馈。

如何参与贡献?

欢迎尝试 next-rspack,报告问题、贡献代码或文档、加入社区讨论。你的任何反馈和贡献都很宝贵。

未来计划

  • 提高测试覆盖率:在下一季度,我们计划将测试覆盖率从当前的 96% 提高到接近 100%。
  • 增强性能:我们将通过原生插件,探索与 Next.js 的更深层次集成,以提升构建性能。
  • 基于用户反馈迭代:继续支持来自 Next.js 生态系统的更多社区插件。
  • 完善集成工作流:建立 Rspack 和 Next.js 之间更加健全的 CI/CD 流程,确保 next-rspack 支持的稳定性和可靠性。
  • 更好的 RSC 支持:Turbopack 的统一模块图解锁了更快、更简单的 RSC 实现。Rspack 将提供类似的 API,为生态带来一流、高性能的 RSC 支持。
  • 模块联邦支持:我们正在与 Next.js 团队讨论改进对模块联邦的支持。

在 2024 年,稳定性和构建产物完整性是 Rspack 的主要关注点。2025 年,我们将更加关注性能提升和广泛的生态系统。

敬请期待 —— 我们的旅程才刚刚开始。

魔改chromium源码——canvas指纹修改 第一节

2025年4月10日 15:03

在进行以下操作之前,请确保已完成之前文章中提到的 源码拉取及编译 部分。

如果已顺利完成相关配置,即可继续执行后续操作。


Canvas 指纹技术简介

Canvas 指纹 是一种基于浏览器的隐式用户标识技术,利用 HTML5 的 元素及其绘图功能生成设备或浏览器的唯一标识符。其核心原理是:不同设备、浏览器或操作系统在渲染相同的图形内容时,可能会产生细微的差异。这些差异可以被提取并转换为一个唯一的“指纹”,从而用于识别用户的设备或浏览器。

Canvas 指纹的实现过程

以字节某音产品中的 Canvas 指纹生成代码为例,分析其实现过程:

function canvasFingerprint(e = 3735928559) {
  var urlStr;
  // 创建 Canvas 元素并设置尺寸
  var canvasEl = document.createElement('canvas');
  canvasEl.width = 100;
  canvasEl.height = 100;

  // 获取绘图上下文并绘制内容
  var canvas = canvasEl.getContext('2d');
  canvas.fillStyle = '#0078d4';
  canvas.fillRect(0, 0, 100, 100);
  canvas.font = '14px serif'; // 设置字体样式
  canvas.fillText('龘ฑภ경', 2, 12); // 绘制多语言字符
  canvas.shadowBlur = 2; // 添加阴影效果
  canvas.showOffsetX = 1; // 自定义属性(非标准)
  canvas.showColor = 'lime'; // 自定义属性(非标准)
  canvas.arc(8, 8, 8, 0, 2); // 绘制圆弧
  canvas.stroke(); // 描边

  // 将 Canvas 内容转换为 Base64 数据 URL
  urlStr = canvasEl.toDataURL();

  // 基于数据 URL 计算哈希值
  for (var i = 0; i < 32; i++) {
    e = (65599 * e + urlStr.charCodeAt(e % urlStr.length)) >>> 0;
  }

  return e; // 返回最终的指纹值
}

// 调用函数生成指纹
canvasFingerprint();

实现步骤解析

  1. 创建 Canvas 元素: 使用 document.createElement('canvas') 创建一个 元素,并设置其宽度为 100 像素,高度为 100 像素。 这些尺寸经过精心设计,既能保证足够的复杂性,又能避免过高的计算开销。

  2. 绘制图形内容: 设置字体样式为 14px serif,并在画布上绘制包含多种语言字符的文本 '龘ฑภ경'。这种多语言字符组合增加了渲染结果的多样性。 添加阴影效果(shadowBlur)和绘制一个绿色圆弧(arc),进一步丰富图形内容。

  3. 提取渲染结果: 使用 canvasEl.toDataURL() 方法将画布内容转换为 Base64 编码的数据 URL。这个 URL 包含了当前设备对图形内容的渲染结果。

  4. 计算哈希值: 遍历数据 URL 的字符序列,通过一个自定义的哈希算法(基于乘法因子 65599 和字符 ASCII 值)计算出一个 32 位整数作为最终的指纹值。 这个哈希值具有高区分度,能够唯一标识当前设备或浏览器。

在src\third_party\blink\renderer\modules\canvas\canvas2d\canvas_2d_recorder_context.cc所在目录中,新建一个random_hex_color.h文件。文件内容如下

#ifndef RANDOM_COLOR_H
#define RANDOM_COLOR_H

#include <array>
#include <random>

inline const char* getRandomColorCode() {
    const std::array<char, 17> hexChars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', '\0'};
    static std::array<char, 8> colorCode;
    static std::mt19937 gen(42);
    static std::uniform_int_distribution<> dis(0, 15);
    colorCode[0] = '#';
    for (int i = 1; i < 7; ++i) {
        colorCode[i] = hexChars[dis(gen)];
    }
    colorCode[7] = '\0';
    return colorCode.data();
}

#endif

在src\third_party\blink\renderer\modules\canvas\canvas2d\canvas_2d_recorder_context.cc目录中,找到底下这段函数的源码。

我们以修改 canvas 的 fillStyle 值为例进行说明。对于其他属性的随机化处理,可以参考同样的思路:找到对应的代码位置,并按照类似的方式调整逻辑。这里不再逐一演示,但方法思路是通用的。

如果在这个文件找不到这个代码,全局搜索一下canvas2d文件夹,不同版本的 chromium 源码结构会有些许差别,但功能是一致的

void Canvas2DRecorderContext::setFillStyle(v8::Isolate* isolate,
                                           v8::Local<v8::Value> value,
                                           ExceptionState& exception_state) {
  ValidateStateStack();

  CanvasRenderingContext2DState& state = GetState();
  // This block is similar to that in setStrokeStyle(), see comments there for
  // details on this.
  if (value->IsString()) {
    // v8::Local<v8::String> v8_string = value.As<v8::String>(); 这行注释
    // 新增的
    const char* color_code = getRandomColorCode(); 
    // 新增的
    v8::Local<v8::String> v8_string = v8::String::NewFromUtf8(isolate, color_code, v8::NewStringType::kNormal).ToLocalChecked(); 
    UpdateIdentifiabilityStudyBeforeSettingStrokeOrFill(
        v8_string, CanvasOps::kSetFillStyle);
    if (state.IsUnparsedFillColor(v8_string)) {
      return;
    }
    Color parsed_color = Color::kTransparent;
    if (!ExtractColorFromV8StringAndUpdateCache(
            isolate, v8_string, exception_state, parsed_color)) {
      return;
    }
    if (state.FillStyle().IsEquivalentColor(parsed_color)) {
      state.SetUnparsedFillColor(isolate, v8_string);
      return;
    }
    state.SetFillColor(parsed_color);
    state.ClearUnparsedFillColor();
    state.ClearResolvedFilter();
    return;
  }
  V8CanvasStyle v8_style;
  if (!ExtractV8CanvasStyle(isolate, value, v8_style, exception_state)) {
    return;
  }

  UpdateIdentifiabilityStudyBeforeSettingStrokeOrFill(v8_style,
                                                      CanvasOps::kSetFillStyle);

  switch (v8_style.type) {
    case V8CanvasStyleType::kCSSColorValue:
      state.SetFillColor(v8_style.css_color_value);
      break;
    case V8CanvasStyleType::kGradient:
      state.SetFillGradient(v8_style.gradient);
      break;
    case V8CanvasStyleType::kPattern:
      if (!origin_tainted_by_content_ && !v8_style.pattern->OriginClean()) {
        SetOriginTaintedByContent();
      }
      state.SetFillPattern(v8_style.pattern);
      break;
    case V8CanvasStyleType::kString: {
      Color parsed_color = Color::kTransparent;
      if (ParseColorOrCurrentColor(v8_style.string, parsed_color) ==
          ColorParseResult::kParseFailed) {
        return;
      }
      if (!state.FillStyle().IsEquivalentColor(parsed_color)) {
        state.SetFillColor(parsed_color);
      }
      break;
    }
  }
  state.ClearUnparsedFillColor();
  state.ClearResolvedFilter();
}

新增头文件

在这里插入图片描述

先将v8::Localv8::String v8_string = value.Asv8::String();这行注释掉,然后新增底下两行代码。

在这里插入图片描述

修改完了之后保存,然后执行以下命令编译

autoninja -C out/Default chrome

创建一个index.html文件,文件内容如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0">
    <title>canvas</title>
</head>
<body>
    <canvas class="" id="myCanvas1" width="100" height="100"></canvas>
    <canvas class="" id="myCanvas2" width="100" height="100"></canvas>
    <script>
        function canvasFingerprint (urlStr) {
            var e = 3735928559
            // 基于数据 URL 计算哈希值
            for (var i = 0; i < 32; i++) {
                e = (65599 * e + urlStr.charCodeAt(e % urlStr.length)) >>> 0;
            }
            return e; // 返回最终的指纹值
        }

        const canvas = document.getElementById('myCanvas1');
        const ctx = canvas.getContext('2d');
        ctx.fillStyle = 'blue';
        ctx.fillRect(0, 0, 100, 100);

        const canvas2 = document.getElementById('myCanvas2');
        const ctx2 = canvas2.getContext('2d');
        ctx2.fillStyle = 'blue';
        ctx2.fillRect(0, 0, 100, 100);
        const urlStr1 = canvas.toDataURL();
        const urlStr2 = canvas2.toDataURL();
        // 调用函数生成指纹
        console.log(canvasFingerprint(urlStr1));
        console.log(canvasFingerprint(urlStr2));
    </script>
</body>
</html>

在编译好的chromium中打开,就可以看到,每次刷新,他的颜色都不一样,已经被我们修改的源码给hook掉了。

在这里插入图片描述在这里插入图片描述在这里插入图片描述


特别说明

通过对上述随机化逻辑的分析和实现,可以发现一种简单而有效的检测方法: 对同一段 Canvas 逻辑代码多次执行,并对比每次生成的结果。如果所有结果完全一致,则说明浏览器环境未被篡改,处于正常状态;反之,如果结果存在不一致,则表明相关函数内容或者浏览器环境已被篡改。

关于这一问题的解决方案,我们将在下一节详细探讨。

三种解决办法 ReferenceError: __dirname is not defined in ES module scope

作者 Legend80s
2025年4月10日 15:01

1 fileURLToPath 结合 import.meta.url

fileURLToPath Added in: v10.12.0 import.meta.url Added in: v10

import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

2. import.meta.dirname ✔️

Added in: v21.2.0, v20.11.0

最少依赖最推荐写法

const __dirname = import.meta.dirname
  • 冷知识 1:import 虽然不能在 cjs 中使用,但是 import.meta 可以
  • 冷知识 2:Node.js 的 LTS 已经是 v22 了,v18 还有三周就要过保了,v20 也只有一年保质期了。LTS 意味着稳定和安全,大家踊跃升级呀。

当前时间数据:2025-4-10

Release Security Support Latest
23 Ends in 1 month and 3 weeks(01 Jun 2025) 23.11.0(01 Apr 2025)
22 (LTS) Ends in 2 years(30 Apr 2027) 22.14.0(11 Feb 2025)
20 (LTS) Ends in 1 year(30 Apr 2026) 20.19.0(13 Mar 2025)
18 (LTS) Ends in 3 weeks(30 Apr 2025) 18.20.8(27 Mar 2025)

来自 endoflife.date/nodejs

可以看下 import.meta 里面还有哪些值

console.log('process.versions:', process.versions.node);
console.log(import.meta);
❯ node import.meta.js
process.versions: 22.7.0
[Object: null prototype] {
  dirname: 'F:\\temp',
  filename: 'F:\\temp\\import.meta.js',
  resolve: [Function: resolve],
  url: 'file:///F:/temp/import.meta.js'
}

3. new URL 结合 import.meta.url

只能在非 Windows 操作系统中用。Macos 系统未尝试大家可以帮忙试试。

import path from 'path'

const __dirname = path.dirname(new URL(import.meta.url).pathname)

Windows 下返回 __dirname: /F:/temp 正确应该是 F:\\temp

更多详见 nodejs.org/docs/latest…

使用libcurl编写爬虫程序指南

2025年4月10日 15:01

用户想知道用Curl库编写的爬虫程序是什么样的。首先,我需要明确Curl本身是一个命令行工具和库,用于传输数据,支持多种协议。而用户提到的“Curl库”可能指的是libcurl,这是一个客户端URL传输库,可以用在C、C++等编程语言中。用户可能想了解如何用libcurl来编写爬虫程序,或者可能混淆了curl命令和编程中的使用。

a4.png

首先,我应该考虑用户可能的背景。他可能对爬虫有一定了解,但可能不太清楚如何具体用libcurl实现。需要区分命令行curl和编程中使用libcurl的区别。用户可能希望看到一个实际的代码示例,比如用C语言结合libcurl来抓取网页内容。

使用 libcurl 库编写的爬虫程序通常涉及以下几个关键步骤。这里以 C 语言为例,展示一个简单的示例程序,并解释其核心逻辑。

示例代码:使用 libcurl 获取网页内容

#include <stdio.h>
#include <curl/curl.h>

// 回调函数:将接收到的数据写入缓冲区
size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {
    size_t real_size = size * nmemb;
    printf("Received %zu bytes of data.\n", real_size);
    // 将数据追加到缓冲区(此处简单打印,实际可保存到内存或文件)
    return real_size;
}

int main(void) {
    CURL *curl;
    CURLcode res;

    // 初始化 libcurl
    curl_global_init(CURL_GLOBAL_DEFAULT);
    curl = curl_easy_init();

    if (curl) {
        // 设置目标 URL
        curl_easy_setopt(curl, CURLOPT_URL, "https://example.com");
        // 设置数据接收回调函数
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);

        // 执行 HTTP 请求
        res = curl_easy_perform(curl);

        // 检查请求结果
        if (res != CURLE_OK) {
            fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        }

        // 清理资源
        curl_easy_cleanup(curl);
    }

    // 全局清理
    curl_global_cleanup();
    return 0;
}

核心步骤解析

  1. 初始化 libcurl

    • curl_global_init():初始化全局 libcurl 环境。
    • curl_easy_init():创建一个 CURL 句柄,用于配置和执行请求。
  2. 配置请求参数

    • CURLOPT_URL:设置目标 URL。

    • CURLOPT_WRITEFUNCTION:指定回调函数处理接收到的数据。

    • 其他常用选项:

      • CURLOPT_FOLLOWLOCATION:跟随重定向(设为 1L 启用)。
      • CURLOPT_USERAGENT:设置用户代理(如 curl_easy_setopt(curl, CURLOPT_USERAGENT, "MyCrawler/1.0"))。
      • CURLOPT_TIMEOUT:设置超时时间。
  3. 执行请求

    • curl_easy_perform():同步执行请求,触发回调函数处理数据。
  4. 处理数据

    • write_callback 中处理接收到的数据(如保存到文件、解析 HTML 等)。
  5. 清理资源

    • curl_easy_cleanup():释放 CURL 句柄。
    • curl_global_cleanup():清理全局环境。

编译与运行

# 编译(需链接 libcurl)
gcc -o crawler crawler.c -lcurl

# 运行
./crawler

进阶功能

  1. 多线程爬虫

    • 使用 curl_multi_init() 实现异步请求,同时处理多个 URL。
  2. 处理 Cookies 和会话

    • 启用 CURLOPT_COOKIEFILECURLOPT_COOKIEJAR 自动管理 Cookies。
  3. 设置代理

    • 通过 CURLOPT_PROXY 指定代理服务器地址。
  4. 解析 HTML

    • 结合第三方库(如 libxml2Gumbo)解析抓取到的 HTML 内容。

注意事项

  • 错误处理:检查 curl_easy_perform() 的返回值(CURLcode)以应对网络错误。
  • 速率限制:避免高频请求,防止被目标网站封禁。
  • 遵守 robots.txt:确保爬虫行为符合目标网站的规则。

如果需要更复杂的逻辑(如并发、动态页面渲染),可能需要结合其他工具(如 SeleniumScrapy),但 libcurl 本身足够高效,适用于高性能、低层次的 HTTP 请求场景。

【kk-utils】键值对双向映射工具——bidirectional-mapping

作者 在掘金
2025年4月10日 14:59

前言

kk-utils 是一款我自己基于这几年开发封装出来的前端工具库

bidirectional-mapping

bidirectional-mapping是kk-utils里的工具之一,其作用是键值对双向映射

它可以把一个字符串、数字、对象、数组按我们的映射规则进行转换

BiMap

用来生成映射规则的一个工具

BiMapConversion

用来把目标数据转换成映射规则处理后的数据

使用方法

安装

npm install kk-utils-library -S
pnpm add kk-utils-library -S

编写映射规则

const map = new BiMap([
  ["a", "A"], // 把'a'转成'A',也可以反过来把'A'转成'a'
  [
    "b",
    "B",
    {
      transformType: String, // 把键名对应的value进行处理,比如转成字符串,转成数字,转成Boolean
    },
  ],
  [
    "c",
    "C", // 数组嵌套数组表示c是复杂对象,里面的数据还要继续转换,c.d会变成C.D
    [
      [
        "d",
        "D",
        {
          transformSourceType: "object", // 转换数据的类型 object、map、array
          transformType: Number,
          transformInverted: true, // 是否可以反过来转换 如为true就会把原本值为'否'变成0
          transform: {
            0: "否",
            1: "是",
          },
        },
      ],
      [
        "e",
        "E",
        {
          transformType: Boolean,
        },
      ],
      [
        "f",
        "F",
        [
          [
            "g",
            "G",
            {
              transformSourceType: "map",
              transformType: Number,
              transformInverted: true,
              transform: new Map([
                [0, "zero"],
                [1, "one"],
              ]),
            },
          ],
          [
            "h",
            "H",
            {
              transformSourceType: "array",
              transformType: Number,
              transformInverted: true,
              transformLabelKey: "label",
              transformValueKey: "value",
              transform: [
                {
                  label: "否",
                  value: 0,
                },
                {
                  label: "是",
                  value: 1,
                },
              ],
            },
          ],
        ],
      ],
    ],
  ],
  [
    "children",
    "CHILDREN",
    {
      recursion: true, // 树结构 递归处理
    },
  ],
  [
    "k",
    "K",
    new BiMap([
      [0, "零"],
      [1, "一"],
    ]), // 深层对象映射可以使用数组嵌套也可以直接使用BiMap BiMap可以处理键名转换也可以处理值的转换
  ],
]);

转换数据

写一个测试数据

const data = [
  {
    a: 1,
    b: 1,
    c: {
      d: 1,
      e: 1,
      f: [
        {
          g: 1,
          h: 1,
        },
      ],
    },
    children: [
      {
        a: 0,
        b: 0,
        c: {
          d: 0,
          e: 0,
          f: [
            {
              g: 0,
              h: 0,
            },
          ],
        },
        k: 0,
      },
      {
        a: 1,
        b: 1,
        c: {
          d: 1,
          e: 1,
          f: [
            {
              g: 1,
              h: 1,
            },
          ],
        },
        k: 1,
      },
    ],
    k: 1,
  },
];

使用映射规则转换数据

const mapData = BiMapConversion(data, map);
console.log(mapData)

转换结果

[
    {
        "A": 1,
        "B": "1",
        "C": {
            "D": "是",
            "E": true,
            "F": [
                {
                    "G": "one",
                    "H": "是"
                }
            ]
        },
        "CHILDREN": [
            {
                "A": 0,
                "B": "0",
                "C": {
                    "D": "否",
                    "E": false,
                    "F": [
                        {
                            "G": "zero",
                            "H": "否"
                        }
                    ]
                },
                "K": "零"
            },
            {
                "A": 1,
                "B": "1",
                "C": {
                    "D": "是",
                    "E": true,
                    "F": [
                        {
                            "G": "one",
                            "H": "是"
                        }
                    ]
                },
                "K": "一"
            }
        ],
        "K": "一"
    }
]

截图对比

截屏2025-04-10 14.35.14.png

可以看到测试数据已经按我定义好的映射规则进行了转换,无论是键名还是值,都转换成功了的

融入业务使用

比如和后端对接接口的时候,后端键名不规范,我们写逻辑的时候就要去重新命名,多处用同一接口的话,就要到处维护,不方便。再比如同一个字段的值一会是字符串,一会是数字,就会对我们的逻辑代码造成不易擦觉得影响,所以可以使用这个工具在接口处统一做出入数据管理

在接口处统一做出入数据管理

比如我要对接后端的反馈接口的CURD

定义公共分页映射

// src/constants/bi-map/common/pagination/index.js
import { BiMap } from 'kk-utils-library/bidirectional-mapping';
// 分页参数
export const $paginationReqItemMap = new BiMap([
  [
    'pagination',
    'pagination',
    [
      ['page', 'pageNumber'],
      ['pageSize', 'pageSize']
    ]
  ]
]);

定义接口映射

// src/constants/bi-map/feedback/index.js
import { BiMap } from 'kk-utils-library/bidirectional-mapping';
import { $paginationReqItemMap } from '../common/pagination';

// 意见反馈列表
export const $feedbackListReqItemMap = new BiMap([
  ['id', 'id'], // ID
  ['type', 'emergencyDegreeType'], // 紧急程度类型
  ['topic', 'theme'], // 主题
  ['content', 'content'] // 内容
]);
export const $feedbackListResItemMap = new BiMap([
  ['id', 'id'], // ID
  ['type', 'emergencyDegreeType'], // 紧急程度类型
  ['topic', 'theme'], // 主题
  ['content', 'content'] // 内容
]);

// 意见反馈分页
export const $feedbackPageReqItemMap = new BiMap([
  ...$feedbackListReqItemMap.sourceData, // 复用 避免多次定义
  ...$paginationReqItemMap.sourceData // 复用 避免多次定义
]);
export const $feedbackPageResItemMap = $feedbackListResItemMap; // 相同映射规则直接指向共用

// 意见反馈详情
export const $feedbackDetailReqItemMap = new BiMap([['id', 'id']]);
export const $feedbackDetailResItemMap = $feedbackListResItemMap;

// 意见反馈新建
export const $feedbackCreateReqItemMap = $feedbackListReqItemMap;
export const $feedbackCreateResItemMap = new BiMap([]);

// 意见反馈编辑
export const $feedbackUpdateReqItemMap = $feedbackListReqItemMap;
export const $feedbackUpdateResItemMap = new BiMap([]);

// 意见反馈删除
export const $feedbackDeleteReqItemMap = new BiMap([['id', 'id']]);
export const $feedbackDeleteResItemMap = new BiMap([]);

接口处使用映射做出入口参数管理

// src/apis/feedback/index.js
import { BiMapConversion } from 'kk-utils-library/bidirectional-mapping';
import {
  $feedbackCreateReqItemMap,
  $feedbackCreateResItemMap,
  $feedbackDeleteReqItemMap,
  $feedbackDeleteResItemMap,
  $feedbackDetailReqItemMap,
  $feedbackDetailResItemMap,
  $feedbackListReqItemMap,
  $feedbackListResItemMap,
  $feedbackPageReqItemMap,
  $feedbackPageResItemMap,
  $feedbackUpdateReqItemMap,
  $feedbackUpdateResItemMap
} from '@/constants/bi-map/feedback';

// 分页获取意见反馈
export function getFeedbackPage(Service) {
  return (params) =>
    new Promise((resolve, reject) => {
      params = BiMapConversion(params || {}, $feedbackPageReqItemMap);
      Service.get('/api/feedbacks', params)
        .then((res) => {
          res.data = BiMapConversion(res.data || {}, $feedbackPageResItemMap);
          resolve(res);
        })
        .catch((error) => reject(error));
    });
}

// 获取意见反馈列表
export function getFeedbackList(Service) {
  return (params) =>
    new Promise((resolve, reject) => {
      params = BiMapConversion(params || {}, $feedbackListReqItemMap);
      Service.get('/api/feedbacks/list', params)
        .then((res) => {
          res.data = BiMapConversion(res.data || [], $feedbackListResItemMap);
          resolve(res);
        })
        .catch((error) => reject(error));
    });
}

// 查询意见反馈详情
export function getFeedbackDetail(Service) {
  return (params) =>
    new Promise((resolve, reject) => {
      params = BiMapConversion(params || {}, $feedbackDetailReqItemMap);
      Service.get(`/api/feedbacks/${params.id}`, { ...params, populate: '*' })
        .then((res) => {
          res.data = BiMapConversion(res.data || {}, $feedbackDetailResItemMap);
          resolve(res);
        })
        .catch((error) => reject(error));
    });
}

// 新建意见反馈
export function createFeedback(Service) {
  return (params) =>
    new Promise((resolve, reject) => {
      params = BiMapConversion(params || {}, $feedbackCreateReqItemMap);
      Service.post('/api/feedbacks', { data: params })
        .then((res) => {
          res.data = BiMapConversion(res.data || {}, $feedbackCreateResItemMap);
          resolve(res);
        })
        .catch((error) => reject(error));
    });
}

// 更新意见反馈
export function updateFeedback(Service) {
  return (params) =>
    new Promise((resolve, reject) => {
      params = BiMapConversion(params || {}, $feedbackUpdateReqItemMap);
      Service.put(`/api/feedbacks/${params.id}`, { data: params })
        .then((res) => {
          res.data = BiMapConversion(res.data || {}, $feedbackUpdateResItemMap);
          resolve(res);
        })
        .catch((error) => reject(error));
    });
}

// 删除意见反馈
export function deleteFeedback(Service) {
  return (params) =>
    new Promise((resolve, reject) => {
      params = BiMapConversion(params || {}, $feedbackDeleteReqItemMap);
      Service.delete(`/api/feedbacks/${params.id}`, params)
        .then((res) => {
          res.data = BiMapConversion(res.data || {}, $feedbackDeleteResItemMap);
          resolve(res);
        })
        .catch((error) => reject(error));
    });
}

这样只需要在接口封装处对params出参做映射转换和接收到数据时对res里的data做映射转换,我们就可以在项目里直接使用自己定义的key,出参给后台的时候也会反过来把我们的key根据映射规则转成后台原本的key再传递给后台的

其他

除了以上业务使用,我们还能用来对单个数据处理

// 我们用回上面定义的new BiMap
const mapping_a = map.get('A') // a
const mapping_A = map.get('a') // A

破解旋转死锁:Threejs 四元数魔法对抗欧拉角困局

作者 伶俜monster
2025年4月10日 14:57

欧拉角

欧拉角是一种表示三维空间中旋转的方法,它由三个角度组成,通过设定物体绕 指定顺序 的轴进行旋转,可以直接对物体的 .rotation 属性进行操作。

rotation 属性是一个欧拉角对象,表示物体的旋转角度。欧拉角由三个角度组成,分别是绕 x 轴的旋转角度、绕 y 轴的旋转角度和绕 z 轴的旋转角度。

// 创建一个立方体
const cube = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
scene.add(cube);

let euler = new THREE.Euler(Math.PI / 2, 0, 0);
cube.rotation.copy(euler);

console.log(cube.rotation);

Euler(x, y, z, order): 创建一个欧拉角对象,其中 x、y、z 分别表示绕 x 轴、y 轴和 z 轴的旋转角度,order 表示旋转顺序。

换算: 欧拉角不能直接使用度数,需要把度数转换为弧度值,弧度 = (Math.PI / 180) * 度数

简洁写法: cube.rotation.set(x, y, z)

注意: 欧拉角不能直接赋值,需要使用 .copy() 方法进行赋值。

// 创建一个立方体
const cube = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
scene.add(cube);

let angle = 0;
function animate() {
  angle += 1;
  cube.rotation.x = (angle * Math.PI) / 180;
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

可以使用 THREE.Euler().set() 来统一操作 .rotation,也可以通过 .x.y.z 来分别操作。

euler.gif

方法

  • rotateX(angle): 绕 x 轴旋转 angle 弧度。

  • rotateY(angle): 绕 y 轴旋转 angle 弧度。

  • rotateZ(angle): 绕 z 轴旋转 angle 弧度。

区别: rotateX()rotateY()rotateZ() 多次使用,会叠加旋转角度,而 set() 每次使用,都会覆盖旋转角度。

四元数

四元数是一种表示三维空间中旋转的方法,它由四个部分组成,分别是 w、x、y、z。其中 w 是实部,x、y、z 是虚部。

为什么使用四元数: 欧拉角在旋转过程中会出现万向节死锁问题,而四元数不会。

万向节死锁

原理: 当其中两个旋转轴重合时,就会发生万向节死锁。这会致使失去一个自由度,进而难以预测和控制物体的旋转。在欧拉角的表示里,通常是当绕其中一个轴旋转 ±90 度时,另外两个轴会重合,从而出现万向节死锁。

例如: 假设一个物体绕 Y 轴旋转 90 度,此时 X 轴和 Z 轴就会重合。这时候,不管是绕 X 轴旋转还是绕 Z 轴旋转,产生的效果是一样的,这就意味着失去了一个自由度,这种现象就是万向节死锁。

//万向节死锁示例
const cube = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
scene.add(cube);

//y轴旋转90度
cube.rotation.y = Math.PI / 2;

let angle = 0;
function animate() {
  angle += 1;
  cube.rotation.x = (angle * Math.PI) / 180;
  cube.rotation.z = (angle * Math.PI) / 180;
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

quaternion

在四元数中是通过操作quaternion来实现的,其中quaternion是一个四元数对象,表示物体的旋转。

quaternion 属性是一个四元数对象,表示物体的旋转。四元数由四个部分组成,分别是 w、x、y、z。其中 w 是实部,x、y、z 是虚部。

1. 创建四元数

const quaternion = new THREE.Quaternion(x, y, z, w);

其中 x、y、z、w 分别是四元数的分量。如果不传入参数,则默认为 (0, 0, 0, 1)。

2. 四元数计算公式

q = (x, y, z, w) = (sin(θ/2) * ux, sin(θ/2) * uy, sin(θ/2) * uz, cos(θ/2))

const angle = (30 * Math.PI) / 180; // 将角度转换为弧度
const halfAngle = angle / 2;
const sinHalfAngle = Math.sin(halfAngle);
const cosHalfAngle = Math.cos(halfAngle);
const quaternion = new THREE.Quaternion(
  sinHalfAngle * 1, //解释
  sinHalfAngle * 0,
  sinHalfAngle * 0,
  cosHalfAngle
);

绕着哪个轴旋转,哪个轴的分量就为 1,其他轴的分量就为 0。

3. 设置四元数

cube.quaternion.copy(quaternion);

简洁写法: setFromAxisAngle(axis, angle) 从轴和角度创建四元数。

1. axis: 表示旋转轴的向量,例如 new THREE.Vector3(1, 0, 0) 表示绕 x 轴旋转。

2. angle: 表示旋转角度,单位是弧度。

const quaternion = new THREE.Quaternion();
const axis = new THREE.Vector3(1, 0, 0); // 绕 x 轴旋转
const angle = (Math.PI / 180) * 90; // 旋转 90 度
quaternion.setFromAxisAngle(axis, angle);

方法

  • setFromEuler(euler): 从欧拉角创建四元数。

  • multiply(quaternion): 将当前四元数与另一个四元数相乘,会改变当前四元数。

  • multiplyQuaternions(a, b): 将两个四元数相乘,返回一个新的四元数。

解决死锁

// 解决万向节死锁示例
const cube = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
scene.add(cube);

//y轴旋转90度
cube.quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);

function animate() {
  // 创建一个四元数qx
  const qx = new THREE.Quaternion();
  // 将qx设置为绕x轴旋转0.01弧度的四元数
  qx.setFromAxisAngle(new THREE.Vector3(1, 0, 0), 0.01);
  // 将cube的旋转四元数与qx相乘
  cube.quaternion.multiplyQuaternions(qx, cube.quaternion);
  // 创建一个四元数qz
  const qz = new THREE.Quaternion();
  // 将qz设置为绕z轴旋转0.01弧度的四元数
  qz.setFromAxisAngle(new THREE.Vector3(0, 0, 1), 0.01);
  // 将cube的旋转四元数与qz相乘
  cube.quaternion.multiplyQuaternions(qz, cube.quaternion);
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

死锁.gif

书洞笔记

记录一次使用mitt库后了解到的type和interface异同

2025年4月10日 14:45

起因是在初始化mitt实例时,发现如果传入interface作为泛型时会抛出异常:

类型“Events”不满足约束“Record<EventType, unknown>”。

类型“Events”中缺少类型“string”的索引签名。ts(2344)。

于是去看了一下传入的泛型Event 的定义:

export declare type EventType = string | symbol;
//  ...
export default function mitt<Events extends Record<EventType, unknown>>(all?: EventHandlerMap<Events>): Emitter<Events>;

这里mitt对传入的泛型Events用extends关键字做了个约束,要求Events必须满足Record<EventType, unknown>,也就是说Events必须是一个键为字符串字面量,值为unknownRecord类型。

其中type有隐式索引签名(implicit index signature)Record<PropertyKey, unknown> 自动满足条件,但是interface是一种静态的类型声明,它并没有隐式索引签名,因此会抛出异常,如果需要使用interface,需要手动添加索引签名:

interface Events {
// ...
[key: string | symbol]: unknown
}

但是如果这样写的话,会出现以下问题:

  • 接口就的类型约束就被破坏了,key可以是任意的字符串,无法确保事件的key是上面编写的字符串字面量;
  • 接口可以被任意扩展,可能添加非法事件;
  • 无法利用类型系统进行静态检查。
interface Events {
  // ...
}

interface Events {
  [key: number]: string // 新增了数字key
}

为什么mitt要用Record<EventType, unknown>来做限制而不是Record<EventType, any>

unknown 和 any 虽然都代表"不确定的类型",但它们在类型安全性上有着本质区别:

  1. unknown 是类型安全的顶级类型

    • 它是 TypeScript 3.0 引入的类型安全的"任意值"表示
    • 任何值都可以赋给 unknown 类型变量
    • 但在使用前必须进行类型检查或类型断言
  2. any 是类型系统的逃生舱

    • 它会完全绕过 TypeScript 的类型检查
    • 可以对其执行任何操作而不触发类型错误
    • 相当于回到了纯 JavaScript 的开发模式

参考资料:

www.totaltypescript.com/type-vs-int…

聚焦应用常用功能,提升用户体验与分发效率

2025年4月10日 14:43

随着HarmonyOS应用的持续发展,应用的功能将越来越丰富,实际上80%的用户使用时长都会集中在20%的特性上,其余的功能可能也仅仅是面向部分用户。
用户在下载应用时,如果应用包含大量的功能和资源,可能会导致下载时间过长;应用如果包含许多不常用或特定用户群体才需要的功能,这些功能会占用用户设备的存储空间;如果应用体积庞大,启动和运行速度可能会受到影响。

为了避免用户首次下载应用耗时过长,及过多占用用户空间,HarmonyOS SDK 应用市场服务(Store Kit)提供 产品特性按需分发的能力,能够提供动态分发和资源拆分,支持用户按需动态下载自己所需的增强特性,减少开发者应用的分发成本,将精力放在维护和分发用户实际需要的功能模块,帮助提高分发效率。

基本概念

按需分发:一个应用程序被打包成多个安装包,安装包包含了所有的应用程序代码和静态资源。用户从应用市场下载的应用只包含基本功能的安装包,当用户需要使用增强功能时,相应安装包将会从服务器下载到设备上。

开发步骤

获取模块安装信息

1.导入moduleInstallManager模块及相关公共模块。

import { moduleInstallManager } from '@kit.StoreKit';

2.构造参数。

入参为需要查询的模块名称。

const moduleName: string = 'AModule';

3.调用getInstalledModule方法,将步骤2中构造的参数传入模块中的getInstalledModule方法。

const moduleInfo: moduleInstallManager.InstalledModule = moduleInstallManager.getInstalledModule(moduleName);

创建按需加载的请求实例

1.导入moduleInstallManager模块及相关公共模块。

import { moduleInstallManager } from '@kit.StoreKit';
import type { common } from '@kit.AbilityKit';

2.构造参数。

入参为当前应用的上下文context,只支持UIAbilityContext和ExtensionContext类型的上下文,其中UIAbilityContext类型的上下文是要校验当前应用是否在前台,如果不在前台,则会被拒绝调用。

const context: common.UIAbilityContext | common.ExtensionContext = getContext(this) as common.UIAbilityContext;

3.调用createModuleInstallRequest方法,将步骤2中构造的参数依次传入模块中的createModuleInstallRequest方法。

const myModuleInstallProvider: moduleInstallManager.ModuleInstallProvider = new moduleInstallManager.ModuleInstallProvider();
const myModuleInstallRequest: moduleInstallManager.ModuleInstallRequest = myModuleInstallProvider.createModuleInstallRequest(context);

请求按需加载的接口

1.导入moduleInstallManager模块及相关公共模块。

import type { common } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { moduleInstallManager } from '@kit.StoreKit';

2.构造参数。

入参为当前要按需加载的模块名。

const moduleNameA: string = 'AModule';
const moduleNameB: string = 'BModule';

3.调用ModuleInstallRequest中的addModule方法,将步骤2中构造的参数依次传入模块中的addModule方法。

let myModuleInstallRequest: moduleInstallManager.ModuleInstallRequest;
try {
  const myModuleInstallProvider: moduleInstallManager.ModuleInstallProvider = new moduleInstallManager.ModuleInstallProvider();
  const context: common.UIAbilityContext | common.ExtensionContext = getContext(this) as common.UIAbilityContext;
  myModuleInstallRequest = myModuleInstallProvider.createModuleInstallRequest(context);
  const aResult: moduleInstallManager.ReturnCode = myModuleInstallRequest.addModule(moduleNameA);
  const bResult: moduleInstallManager.ReturnCode = myModuleInstallRequest.addModule(moduleNameB);
  hilog.info(0, 'TAG', 'aResult:' + aResult + ' bResult:' + bResult);
} catch (error) {
  hilog.error(0, 'TAG', `addModule onError.code is ${error.code}, message is ${error.message}`);
}

4.调用fetchModules方法,将步骤三中的myModuleInstallRequest传入模块中的fetchModules方法。

try {
  moduleInstallManager.fetchModules(myModuleInstallRequest)
    .then((data: moduleInstallManager.ModuleInstallSessionState) =&gt; {
      hilog.info(0, 'TAG', 'Succeeded in fetching Modules data.');
    })
} catch (error) {
  hilog.error(0, 'TAG', `fetching Modules onError.code is ${error.code}, message is ${error.message}`);
}

使用动态模块

假如应用A由entry.hap、AModulelib.hsp两个包组成,其中entry是基础包,AModulelib扩展是功能包(创建方式请参考应用程序包开发与使用)。通过应用市场下载安装只会下载安装entry包,在entry包里面可以通过fetchModules接口动态下载AModulelib包,并使用动态import技术调用AModulelib里的方法和组件。

AModulelib中主要实现如下:

  • 在动态模块AModulelib中定义add方法和DateComponent组件。其中add方法用于计算加法,DateComponent用于显示文本。

Calc.ets定义如下:

export function add(a:number, b:number) {
  return a + b;
}

DateComponent.ets定义如下:

@Component
struct DateComponent {
  build() {
    Column() {
      Text('我是AModulelib中的组件')
        .margin(10);
    }
    .width(300).backgroundColor(Color.Yellow);
  }
}


@Builder
export function showDateComponent() {
  DateComponent()
}
  • 在AModulelib的AModulelib/Index.ets中导出add方法和showDateComponent方法。

    export { add } from './src/main/ets/utils/Calc'; export { showDateComponent } from './src/main/ets/components/DateComponent';

entry中主要实现如下:

  • 在entry基础模块中,增加动态依赖配置。entry的oh-package.json5中使用dynamicDependencies来动态依赖AModulelib模块。

    { "dynamicDependencies": { "AModulelib": "file:../AModulelib" } }

  • 在entry中使用动态模块AModulelib模块里面的方法和组件。在调用AModulelib中的功能前需要判断AModulelib是否已经加载,未加载时请参考请求按需加载的接口完成加载。

    import { moduleInstallManager } from '@kit.StoreKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { BusinessError, Callback } from '@kit.BasicServicesKit'; import { common } from '@kit.AbilityKit'; import { promptAction } from '@kit.ArkUI';

    const TAG: string = 'TAG';

    @Entry @Component struct Index { @BuilderParam AModulelibComponent: Function; @State countTotal: number = 0; @State isShow: boolean = false;

    build() { Row() { Column() { Button(调用增量模块中的add功能:3+6) .onClick(() => { this.initAModulelib(() => { import('AModulelib').then((ns: ESObject) => { this.countTotal = ns.add(3, 6); }).catch((error: BusinessError) => { hilog.error(0, 'TAG', add onError.code is ${error.code}, message is ${error.message}); }) }) }); Text('计算结果:' + this.countTotal) .margin(10); Button(调用增量模块中的showDateComponent功能) .onClick(() => { this.initAModulelib(() => { import('AModulelib').then((ns: ESObject) => { this.AModulelibComponent = ns.showDateComponent; this.isShow = true; }).catch((error: BusinessError) => { hilog.error(0, 'TAG', showDateComponent onError.code is ${error.code}, message is ${error.message}); }) }) }).margin({ top: 10, bottom: 10 }); if (this.isShow) { this.AModulelibComponent() } } .width('100%') } .height('100%') }

    private showToastInfo(msg: string) { promptAction.showToast({ message: msg, duration: 2000 }); }

    /**

    • 检查是否已加载AModulelib包
    • @param successCallBack 回调 */ private initAModulelib(successCallBack: Callback): void { try { const result: moduleInstallManager.InstalledModule = moduleInstallManager.getInstalledModule('AModulelib'); if (result?.installStatus === moduleInstallManager.InstallStatus.INSTALLED) { hilog.info(0, TAG, 'AModulelib installed'); successCallBack && successCallBack(); } else { // AModulelib模块未安装, 需要调用fetchModules下载AModulelib模块。 hilog.info(0, TAG, 'AModulelib not installed'); this.fetchModule('AModulelib', successCallBack) } } catch (error) { hilog.error(0, 'TAG', getInstalledModule onError.code is ${error.code}, message is ${error.message}); } }

    /**

    • 添加监听事件
    • @param successCallBack 回调 / private onListenEvents(successCallBack: Callback): void { const timeout = 3 * 60; //单位秒, 默认最大监听时间为30min(即3060秒) moduleInstallManager.on('moduleInstallStatus', (data: moduleInstallManager.ModuleInstallSessionState) => { // 返回成功 if (data.taskStatus === moduleInstallManager.TaskStatus.INSTALL_SUCCESSFUL) { successCallBack && successCallBack(); this.showToastInfo('install success'); } }, timeout) }

    /**

    • 加载指定包
    • @param moduleName 需要加载的安装包名称
    • @param successCallBack 回调 */ private fetchModule(moduleName: string, successCallBack: Callback) { try { hilog.info(0, TAG, 'handleFetchModules start'); const context = getContext(this) as common.UIAbilityContext; const moduleInstallProvider: moduleInstallManager.ModuleInstallProvider = new moduleInstallManager.ModuleInstallProvider(); const moduleInstallRequest: moduleInstallManager.ModuleInstallRequest = moduleInstallProvider.createModuleInstallRequest(context); if (!moduleInstallRequest) { hilog.warn(0, TAG, 'moduleInstallRequest is empty'); return; } moduleInstallRequest.addModule(moduleName); moduleInstallManager.fetchModules(moduleInstallRequest) .then((data: moduleInstallManager.ModuleInstallSessionState) => { hilog.info(0, TAG, 'Succeeded in fetching Modules result.'); if (data.code === moduleInstallManager.RequestErrorCode.SUCCESS) { this.onListenEvents(successCallBack) } else { hilog.info(0, TAG, 'fetchModules failure'); } }) .catch((error: BusinessError) => { hilog.error(0, 'TAG', fetchModules onError.code is ${error.code}, message is ${error.message}); }) } catch (error) { hilog.error(0, 'TAG', handleFetchModules onError.code is ${error.code}, message is ${error.message}); } } }

行业拓展

分享一个面向研发人群使用的前后端分离的低代码软件——JNPF

基于 Java Boot/.Net Core双引擎,它适配国产化,支持主流数据库和操作系统,提供五十几种高频预制组件,内置了常用的后台管理系统使用场景和实用模版,通过简单的拖拉拽操作,开发者能够高效完成软件开发,提高开发效率,减少代码编写工作。

JNPF基于SpringBoot+Vue.js,提供了一个适合所有水平用户的低代码学习平台,无论是有经验的开发者还是编程新手,都可以在这里找到适合自己的学习路径。

此外,JNPF支持全源码交付,完全支持根据公司、项目需求、业务需求进行二次改造开发或内网部署,具备多角色门户、登录认证、组织管理、角色授权、表单设计、流程设计、页面配置、报表设计、门户配置、代码生成工具等开箱即用的在线服务。

Web Speech API实现语音的识别和播放

作者 三小河
2025年4月10日 14:40

背景

在进行大模型的开发项目中,有很多会涉及到语音的识别及语音的播放,现按照 Web Speech api原生支持的是,实现实现此功能,此demo仅仅在科学上网的情况下,览器默认调用的语音识别服务有网络限制

实现语音的识别

<template>
  <div>
    <button @click="startRecognition" :disabled="isListening">开始录音</button>
    <button @click="stopRecognition" :disabled="!isListening">停止</button>
    <p>识别结果: {{ recognitionText }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue';


const isListening = ref(false);
const recognitionText = ref('');
let recognition;


const startRecognition = () => {
  // 检查浏览器是否支持
  if (!('webkitSpeechRecognition' in window)) {
    alert('您的浏览器不支持语音识别');
    return;
  }

  recognition = new window.webkitSpeechRecognition();
  recognition.continuous = true;
  recognition.interimResults = true;
  recognition.lang = 'zh-CN'; // 设置为中文

  recognition.onstart = () => {
    isListening.value = true;
  };

  recognition.onresult = (event) => {
    let interimTranscript = '';
    let finalTranscript = '';

    for (let i = event.resultIndex; i < event.results.length; i++) {
      const transcript = event.results[i][0].transcript;
      if (event.results[i].isFinal) {
        finalTranscript += transcript;
      } else {
        interimTranscript += transcript;
      }
    }

    recognitionText.value = finalTranscript || interimTranscript;
  };

  recognition.onerror = (event) => {
    console.error('语音识别错误:', event.error);
    isListening.value = false;
  };

  recognition.onend = () => {
    isListening.value = false;
  };

  recognition.start();
};

const stopRecognition = () => {
  if (recognition) {
    recognition.stop();
  }
};

</script>

语音的识别及播放


<template>
  <div>
    <button @click="startRecognition">开始语音识别</button>
    <button @click="stopRecognition">停止语音识别</button>
    <button @click="speakText">播放文字语音</button>
    <textarea v-model="recognizedText" rows="4" cols="50" placeholder="识别结果将显示在这里"></textarea>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const recognizedText = ref('');
const recognition = ref(null);
const synth = ref(window.speechSynthesis);

// 初始化语音识别
const initRecognition = () => {
  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
  recognition.value = new SpeechRecognition();
  recognition.value.lang = 'zh-CN';

  recognition.value.onresult = (event) => {
    const transcript = event.results[0][0].transcript;
    recognizedText.value = transcript;
  };

  recognition.value.onerror = (event) => {
    console.error('语音识别出错:', event.error);
  };
};

// 开始语音识别
const startRecognition = () => {
  if (!recognition.value) {
    initRecognition();
  }
  recognition.value.start();
};

// 停止语音识别
const stopRecognition = () => {
  if (recognition.value) {
    recognition.value.stop();
  }
};

// 播放文字语音
const speakText = () => {
  if (recognizedText.value) {
    const utterance = new SpeechSynthesisUtterance(recognizedText.value);
    utterance.lang = 'zh-CN';
    synth.value.speak(utterance);
  }
};
</script>

<style scoped>
button {
  margin: 5px;
}
</style>

缺点

语音的识别需要 科学上网才可以,否则无法识别, 语音的播放是都可以的

Tauri框架使用Acrylic特效,拖动卡顿的解决方案

作者 XuanXu
2025年4月10日 14:28

最近使用tauri框架进行软件开发,碰上了不少的坑。tauri还算是一个比较新的框架,很多问题遇到了都找不到解决办法,遂记录下来方便自己,也希望遇到同样问题的同学可以找到问题的答案。

tauri使用window-vibrancy在Windows 10下拖动窗口卡顿问题

这个问题在window-vibrancy作者的README中也提示了。

Function Supported platforms Notes
apply_blur&clear_blur Windows 7/10/11 (22H1 only) Bad performance when resizing/dragging the window on Windows 11 build 22621+.
apply_acrylic&clear_acrylic Windows 10/11 Bad performance when resizing/dragging the window on Windows 10 v1903+ and Windows 11 build 22000.
apply_mica&clear_mica Windows 11
apply_vibrancy&clear_vibrancy macOS 10.10 and newer

我的解决办法是检测Windows的版本,如果是有性能问题的版本,则在拖动窗口时先清除acrylic特效,然后拖动结束后再apply回来。

  // 添加检查 Windows 版本的函数
  const hasAcrylicPerformanceIssueVersion = () => {
    const plat = platform();
    const ver = version();

    if (plat !== "windows") return false;

    // 解析版本号
    const [major, minor, build] = ver.split(".").map(Number);

    // https://learn.microsoft.com/en-us/windows/release-health/windows11-release-information
    // Windows 10 v1903+ (build 18362+) 到 Windows 10 最后一个版本
    const isWin10WithIssue =
      major === 10 && minor === 0 && build >= 18362 && build < 22000;

    // Windows 11 build 22000
    const isWin11WithIssue = major === 10 && minor === 0 && build === 22000;

    return isWin10WithIssue || isWin11WithIssue;
  };

上面用到的是tauri的os-infoOS Infomation | Tauri插件,获取Windows的版本号,检查是否为有性能问题的版本。

这样就出现了第二个问题,data-tauri-drag-region会导致鼠标的事件被吞[bug] Dragging window causes focus toggling, plus eats mouse event(s) · Issue #10767 · tauri-apps/tauri,就导致我们监听不到鼠标释放的事件,不知道什么时候拖动结束,将acrylic特效清除后就加不回来了。

最后找了很久,想到了一个曲线救国的办法,利用GetAsyncKeyState全局检测鼠标左键。

解决方案

首先监听onMouseDown事件,点击后就清除acrylic特效,随后利用Windows API,使用GetAsyncKeyState检测鼠标左键是否已经释放,如果释放,则调用applyAcrylic将窗口特效添加回来。虽然谈不上优雅的解决方案,但是至少没有特别大的问题。

const handleDrag = async (e: React.MouseEvent) => {
    if (e.button === 0) {
      const hasPerformanceIssue = hasAcrylicPerformanceIssueVersion();
      if (hasPerformanceIssue) {
        dispatch(setWindowVibrancy(false));
        clearWindowAcrylic();
      }

      const appWindow = getCurrentWindow();
      await appWindow.startDragging();

      if (hasPerformanceIssue) {
        // 每100ms检查一次鼠标左键状态,如果为放开状态,则恢复窗口模糊
        const interval = setInterval(() => {
          getSwapButtonState().then((state) => {
            if (state) {
              getAsyncKeyState(0x02).then((mouseState) => {
                if (mouseState >= 0) {
                  restoreWindowVibrancy();
                  // 停止检查
                  clearInterval(interval);
                }
              });
            } else {
              getAsyncKeyState(0x01).then((mouseState) => {
                if (mouseState >= 0) {
                  restoreWindowVibrancy();
                  // 停止检查
                  clearInterval(interval);
                }
              });
            }
          });
        }, 100);
      }
    }
  };

上面就是修改之后的拖动监听函数。这里调用了两个Windows API

GetSystemMetrics检查鼠标调换

getSwapButtonState()实际上调用的是GetSystemMetrics function (winuser.h) - Win32 apps | Microsoft Learn,首先需要检测系统是否调换了鼠标左右键,在GetAsyncKeyState function (winuser.h) - Win32 apps | Microsoft Learn的Remarks中也提到了,GetAsyncKeyState直接检查鼠标的物理按键,而不是逻辑按键,因此如果系统调换了左右键,GetAsyncKeyState获取的结果就对不上了。

GetAsyncKeyState检查左键是否释放

GetAsyncKeyState function (winuser.h) - Win32 apps | Microsoft Learn接受一个参数,案件的virtual key,具体的key code在这里Virtual-Key Codes (Winuser.h) - Win32 apps | Microsoft Learn。0x01代表鼠标左键,0x02代表鼠标右键

因此上面的代码就是检查鼠标是否调换,如果调换了就调用GetAsyncKeyState(0x02),否则调用GetAsyncKeyState(0x01)进行检查。

GetAsyncKeyState的返回结果官方也做了较为详细的说明,返回类型为SHORT

If the most significant bit is set, the key is down, and if the least significant bit is set, the key was pressed after the previous call to GetAsyncKeyState

如果最高位被设置了,则该键被按下,如果最低位被设置了,则该键从上次调用GetAsyncKeyState后被按下了。

我们在这里需要解决的是获取键是否被释放,以便我们恢复窗口的acrylic特效。根据上面的介绍就可以知道,如果最高位被设置了就是被按下,则最高位没被设置就是处于释放状态。

由于返回值类型为SHORT,是一个有符号整数,因此最高位为符号位,所以我们只需要判断返回值是否大于等于0,就可以知道按键是否处于释放状态。

前端调用Windows API

由于tauri的后端是rust,所以这里我使用的是windows - crates.io: Rust Package Registry,只需要在后端写上调用Windows API的代码,然后使用tauri的#[tauri::command]宏,就可以轻松调用后端代码了。

从前端调用 Rust | Tauri - Tauri 框架

pub fn get_async_key_state(key: i32) -> Result<i16, Box<dyn std::error::Error>> {
    //使用windows crate的GetAsyncKeyState函数
    let state = unsafe { windows::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState(key) };
    Ok(state)
}

然后去注册这个方法成为commands

#[tauri::command]
pub fn get_async_key_state(key: i32) -> i16 {
    match hotkey_listener::get_async_key_state(key) {
        Ok(ret) => ret,
        Err(e) => {
            eprintln!("Failed to get async key state, Error: {}", e.to_string());
            return 0;
        }
    }
}

在main函数的invoke_handler中注册这个get_async_key_state,之后就可以在javascript中使用invoke调用了。

export function getAsyncKeyState(key: number) {
  return invoke<any>("get_async_key_state", {
    key,
  });
}

至此,我们就已经解决了这个问题。首先通过onMouseDown检测鼠标按下,如果是受影响的Windows版本,则清除窗口的特效,然后使用setInterval()开始循环调用GetAsyncKeyState(),直到用户停止拖动,释放鼠标左键的那一刻,GetAsyncKeyState()的返回值由负数变为正数,恢复窗口的特效,退出setInterval()。

Vue 3 国际化实战:支持 Element Plus 组件和语言持久化

2025年4月10日 14:27

Vue 3 国际化实战:支持 Element Plus 组件和语言持久化

i18n来源于英文单词internationalization的首末字母in,18为中间的字符数,是"国际化"的简称,在资迅领域,国际化(i18n)指产品,出版物,软件,硬件等无需做大的改变就能适应不同的语言和地区的需要,对前端开发来说,在不修改内部代码的情况下,能根据不同语言及地区显示相应的界面,在全球化时代,国际化尤为重要,因为产品的潜在用户可能来自世界各个国家,通常与i18n相关的还有L10n本地化的简称。

实现效果:

效果一、中英文切换

Vue 3 国际化实战:支持 Element Plus 组件和语言持久化

效果二、本地持久化存储

Vue 3 国际化实战:支持 Element Plus 组件和语言持久化

效果三、element Plus国际化

Vue 3 国际化实战:支持 Element Plus 组件和语言持久化

vue3项目国际化实现步骤

第一步、安装i18n

vue3中需要安装的版本是v9版本

 npm install vue-i18n@9

Vue 3 国际化实战:支持 Element Plus 组件和语言持久化Vue 3 国际化实战:支持 Element Plus 组件和语言持久化

第二步、配置i18nenzh

  1. 创建src/locales/en.json
    {
      "header": {
        "title": "JuBo Vision · AI Intelligent Inspection System",
        "home": "Home",
        "systemConfiguration": "System Configuration",
        "inspectionSettings": "Inspection Settings",
        "faultConfiguration": "Fault Configuration",
        "logs": "Logs"
      }
    }
    
  2. 创建src/locales/zh.json
    {
      "header": {
        "title": "聚玻Vision ·AI智能检测系统",
        "home": "首页",
        "systemConfiguration": "系统配置",
        "inspectionSettings": "检测配置",
        "faultConfiguration": "故障配置",
        "logs": "日志"
      }
    }
    

第三步:使用 vue-i18n 库来实现多语言支持

创建src/locales/index.ts,其中代码步骤:

  1. vue-i18n中导入用于创建 i18n 实例的方法。
  2. 分别导入中文和英文的语言资源文件(enzhJSON 格式)。
  3. 通过navigator.language获取用户语言环境。
  4. 检查本地缓存的语言设置,如果本地存储中有值,则解析 JSON 数据并提取 curLocale 字段作为当前语言。
  5. 创建 i18n 实例。
  6. 定义支持的语言列表。

整体代码:

import { createI18n } from 'vue-i18n'
import zh from './zh.json'
import en from './en.json'

// 获取浏览器界面语言,默认语言
let currentLanguage = navigator.language.replace(/-(\S*)/, '')

// 如果本地缓存记录了语言环境,则使用本地缓存
let lsLocale = localStorage.getItem('locale') || ''
if (lsLocale) {
  currentLanguage = JSON.parse(lsLocale)?.curLocale
}

export const i18n = createI18n({
  locale: currentLanguage,
  legacy: false, // 修复组件引入i18n时vite脚手架报错的问题
  globalInjection: true, // 全局注册 $t
  messages: {
    zh,
    en
  }
})

export const langs = [
  { key: 'zh', title: '中文' },
  { key: 'en', title: 'English' }
]

第四步、本地持久化

当我们把中文改成English后,再刷新页面,语言环境又会回到中文,所以我们需要做一点工作来保持国际化状态。 创建src/stores/locales.ts文件,代码如下:

import { defineStore } from 'pinia'
import { ref } from 'vue'
import { i18n } from '../locales/index'
export const useLocaleStore = defineStore(
  'locale',
  () => {
    // 初始化时从 localStorage 获取语言设置,默认为 'en'
    const locale = ref(localStorage.getItem('user-locale') || i18n.global.locale.value);

    // 设置locale
    function setLocale(lang: any) {
      locale.value = lang
      i18n.global.locale.value = lang
      localStorage.setItem('user-locale', lang); // 持久化存储到 localStorage
    }
    return { locale, setLocale }
  }
)

第五步、main.ts挂载

import { createApp, watch } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
// 整合ElementPlus
import ElementPlus from 'element-plus';
// 样式文件,elemen-plus样式在index.scss中导入了
import '@/styles/index.scss';
// 图标
import { useElIcon } from '@/utils/setGlobal';
// 国际化
import { i18n } from './locales/index'
// 引入 Pinia Store
import { useLocaleStore } from './stores/locales';
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.use(i18n)
app.use(ElementPlus);
// 初始化语言设置
const localeStore = useLocaleStore();
const currentLocale = localeStore.locale;
// 同步 i18n 的语言设置
localeStore.setLocale(currentLocale);
// 全局注册图标`ele-`开头(样式在index.scss中)
useElIcon(app);
app.mount('#app')

第六步、页面使用测试效果

代码:

<template>
  <!-- 右侧头部区域 -->
  <el-header class="layout-header">
    <div class="header-box">
      <logo />
    </div>
    <div
      class="header-navigation-bar"
      style="display: flex; justify-content: space-between"
    >
      <div style="width: 50%">
        <el-menu
          :router="true"
          :default-active="$route.path"
          class="el-menu-demo"
          mode="horizontal"
          @select="handleSelect"
        >
          <el-menu-item index="/">{{ $t("header.home") }}</el-menu-item>
          <el-menu-item index="/systemSetting">{{
            $t("header.systemConfiguration")
          }}</el-menu-item>
          <el-sub-menu index="/detectionConfig">
            <template #title>{{ $t("header.inspectionSettings") }}</template>
            <el-menu-item index="/detectionConfig">{{
              $t("header.inspectionSettings")
            }}</el-menu-item>
            <el-menu-item index="/faultConfig">{{
              $t("header.faultConfiguration")
            }}</el-menu-item>
          </el-sub-menu>
          <el-menu-item index="/log">
            {{ $t("header.logs") }}
          </el-menu-item>
        </el-menu>
      </div>
      <el-date-picker type="datetime" placeholder="Select date and time" />
      <div style="margin-right: 50px; height: 30px">
        <el-dropdown
          @command="handleCommand"
          style="width: 80px; line-height: 30px"
        >
          <div class="el-dropdown-link" style="display: flex">
            <div>{{ currentLan }}</div>
            <div style="line-height: 30px; line-height: 37px; margin-left: 4px">
              <SvgIcon name="ele-ArrowDown"></SvgIcon>
            </div>
          </div>

          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item
                v-for="lang in langs"
                :key="lang.key"
                :command="lang"
                >{{ lang.title }}</el-dropdown-item
              >
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </div>
    </div>
  </el-header>
</template>
<script setup lang="ts" name="LayoutHeader">
import { defineAsyncComponent } from "vue";
import { ref } from "vue";

//正确用法
import { langs } from "../../locales/index"; //这个是你自己的国际化配置文件

import { useLocaleStore } from "../../stores/locales";
import { getCurrentInstance } from "vue";
const Logo = defineAsyncComponent(() => import("./logo.vue"));
const useLocale = useLocaleStore();

const handleSelect = (key: string, keyPath: string[]) => {
  console.log(key, keyPath);
};
const app = getCurrentInstance();
let curLocale = useLocale.locale;
let currentLan = ref(langs.find((cur) => cur.key === curLocale)?.title || "");

const handleCommand = (command: any) => {
  currentLan.value = command.title;
  // i18n.global.locale.value = command.key; // 设置初始语言
  useLocale.setLocale(command.key);
};
</script>
<style scoped lang="scss">
.el-menu--horizontal {
  --el-menu-horizontal-height: 30px;
}
.header-box {
  height: 52px;
  width: 100;
  background-color: white;
  line-height: 52px;
  border-bottom: 1px solid #f3f3f3;
}
.header-navigation-bar {
  height: 30px;
  width: 100%;
}
</style>

效果:Vue 3 国际化实战:支持 Element Plus 组件和语言持久化

第七步、 Element Plus的国际化操作

使用 Element Plus 提供的语言切换 APIElLocaleProviderel-config-provider 组件),可以实现 Element Plus 组件的动态国际化!你只需要在 App.vue 中包一层配置组件就可以了。

<script setup lang='ts'>
import { computed } from "vue";
import { useLocaleStore } from "./stores/locales";
import zhCn from "element-plus/es/locale/lang/zh-cn";
import en from "element-plus/es/locale/lang/en";

const localeStore = useLocaleStore();
const currentLocale = computed(() => {
  return localeStore.locale === "zh" ? zhCn : en;
});
</script>
<template>
  <!-- 不要少了`class="h100"` 高度100%,不然布局的高度无法占满 -->
  <div class="h100">
    <!-- 路由组件渲染出品 -->
    <!-- <router-view></router-view> -->
    <el-config-provider :locale="currentLocale">
      <router-view />
    </el-config-provider>
  </div>
</template>
<style scoped>
</style>

第八步、element Plus国际化效果测试

Vue 3 国际化实战:支持 Element Plus 组件和语言持久化 刷新持久化存储: Vue 3 国际化实战:支持 Element Plus 组件和语言持久化 完结~

【javascript】generator函数

2025年4月10日 14:26

JavaScript 中的 Generator 函数 是一种特殊的函数类型,它能够中断并恢复执行,这使得它在处理复杂的异步逻辑、协程等场景中非常有用。它的行为与普通的函数不同,因为它可以在执行过程中暂停,允许函数在某个点上“暂停”执行,之后可以继续从暂停点执行。 @TOC

什么是 Generator 函数?

Generator 函数通过 function* 声明,返回一个特殊的 Generator 对象,其核心机制是 yield 关键字。这个对象有一个可以“暂停”并在以后恢复执行的能力。

当调用一个 Generator 函数时,它不会立即执行,而是返回一个 Generator 对象

调用next()函数在每个 yield 语句处暂停。当我们调用 next() 时,它会继续执行到下一个 yield

例子:

function* count() {
  console.log("Start");
  yield 1;
  console.log("Continue");
  yield 2;
  console.log("End");
  yield 3;
}

const counter = count();// 返回 Generator 对象
console.log(counter.next()); // Start 1
console.log(counter.next()); // Continue 2
console.log(counter.next()); // End 3

1.next() 方法

作用

  • next() 是控制 Generator 执行的主要方法,它会恢复 Generator 函数的执行,直到下一个 yield
  • 每次调用 next(),都会返回一个对象 { value, done }
    • value: 当前 yield 表达式的返回值。
    • done: 布尔值,指示生成器是否已完成执行。

用法与返回值

function* gen() {
  yield 1;
  yield 2;
  return 3;
}

const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // { value: 3, done: true }
console.log(g.next()); // { value: undefined, done: true }
  • g.next():调用 next() 时,Generator 会继续执行,直到遇到下一个 yield 或函数结束。如果没有 yield,它会直接返回一个 done: true 对象。

参数传递

next() 可以接收一个参数,这个参数会作为上一次 yield 表达式的返回值,传递给生成器。


function* gen() {
  const a = yield 'What is your name?';
  const b = yield 'What is your age?';
  return `${a} is ${b} years old.`;
}

const g = gen();
console.log(g.next());            // { value: 'What is your name?', done: false }
console.log(g.next('Alice'));     // { value: 'What is your age?', done: false }
console.log(g.next(30));          // { value: 'Alice is 30 years old.', done: true }

在上述代码中,第一次 next() 返回一个字符串 'What is your name?'。当我们向生成器传递参数('Alice'30)时,参数会依次传递给 ab,最终返回 "Alice is 30 years old."

3. throw() 方法

作用

  • throw() 方法可以用来在 Generator 函数中抛出错误。
  • 它会中断当前的 yield 并跳到最近的 catch 块中。如果 Generator 中没有 try-catch 语句捕获错误,那么 throw() 会终止执行,并返回一个 done: true 的结果。

用法与返回值

function* gen() {
  try {
    yield 1;
  } catch (err) {
    console.log(err); // 捕获到错误
    yield 2;
  }
  yield 3;
}

const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.throw("Oops!")); // "Oops!" { value: 2, done: false }
console.log(g.next()); // { value: 3, done: true }
  • g.throw("Oops!"):在执行到 yield 1 时,抛出一个错误 Oops!,这个错误会被 try-catch 捕获,并在捕获后继续执行 yield 2。如果 throw() 被调用的时机不合适(比如没有 try-catch),生成器将停止执行并返回一个 { value: undefined, done: true } 对象。

没有 catch 时的行为

function* gen() {
  yield 1;
  yield 2;
}

const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.throw("Error")); // Error { value: undefined, done: true }

在这个例子中,调用 g.throw() 会导致生成器抛出错误并终止执行,因为没有 catch 语句来捕获该错误。

4. return() 方法

作用

  • return() 用于提前结束 Generator 函数,并返回一个结果。
  • return() 会使得生成器结束执行,并返回 { value, done: true } 对象,done 属性为 true
  • 返回的 value 会作为返回值传递给 return() 调用的地方。

用法与返回值

function* gen() {
  yield 1;
  yield 2;
  return 3;
}

const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.return(4)); // { value: 4, done: true }
console.log(g.next()); // { value: undefined, done: true }
  • g.return(4):这个方法会立即结束生成器,并返回 { value: 4, done: true }。注意:如果在 Generator 函数中调用 return,它会停止 Generator 的执行。

5. next(), throw(), return() 的总结与对比

方法 作用 返回值 适用场景
next() 恢复执行 Generator,直到下一个 yield 或结束 { value: any, done: boolean } 用于继续执行 Generator,并返回下一个 yield 的值。
throw() 在 Generator 中抛出一个错误 { value: any, done: boolean } 用于处理错误,并在 Generator 内部抛出异常。
return() 结束 Generator 函数的执行 { value: any, done: true } 用于提前结束 Generator,并返回结果。

总结

Generator 函数和其方法(next(), throw(), return())提供了对函数执行流程的精确控制。通过 yield 暂停执行,next() 恢复执行,throw() 处理错误,return() 提前结束生成器,这使得 Generator 函数在处理异步编程、协程、流控制等场景中非常有用。掌握这些方法,可以帮助开发者更好地控制异步操作和复杂逻辑的执行过程。

鸿蒙系统中实现保存pdf至本地

作者 Star768
2025年4月10日 14:13

最近开发过程中遇到这样一个需求,需要将发票保存到本地,在网上看了一些案例,没有实现所需要的功能,并且都需要声明额外权限,于是我决定自己实现,在这里记录下我的实现过程。

pdf 展示方式

在下载之前首先是将 pdf 展示出来,ArkUI 中有一个 Web 组件,可以非常方便的实现这个功能。 Web 组件的用途就是用来展示和下载 web 内容,其能力由@ohos.web.webview 提供,因此也可以通过 url 来展示和下载 pdf。

Web({
  src: this.pdfUrl, // 加载网络PDF文档
  controller: this.controller,
}).domStorageAccess(true); // domStorageAccess 方法用于控制Web中对文档对象模型存储(DOM Storage API)的启用状态,若将其设置为 false,会影响到PDF文件在Web中的预览功能

实现思路

由于发票是由第三方平台所返回的,格式不固定,存在图片格式及已.pdf 结尾的 PDF 文件和无后缀的 PDF 文件,因此需要先判断文件类型,然后进行不同的处理。第二步下载文件到应用沙箱,第三部利用文件选择器将文件复制到公共目录下。

首先是通过url后缀判断文件类型

function getFileExtension(url: string): string {
  // 去除URL中的查询参数和锚点
  const cleanUrl = url.split('?')[0].split('#')[0];
  const filename = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1);
  // 提取扩展名(带点判断)
  const lastDotIndex = filename.lastIndexOf('.');
  if (lastDotIndex > 0) {
    // 确保点不在文件名开头
    return filename.substring(lastDotIndex).toLowerCase();
  }
  return '';
}

然后第二步,就是先将文件下载到沙箱中

 @State pdfUrl: string = '' // pdf地址
 ... // 省略其他代码 
 // 保存到本地沙箱
async saveToSandbox(){
      const sandboxDir = getContext().cacheDir;
    // 创建唯一文件名
      const fileExtension = this.getFileExtension(this.pdfUrl);
      const fileName = `invoice_${new Date().getTime()}${fileExtension ? fileExtension : '.pdf'}`;
      const fullPath = `${sandboxDir}/${fileName}`; // 直接拼接路径
      // 下载文件到沙箱中
      await this.downloadPDF(fullPath)
      // 保存到Download中
      this.copyToDownload(fileName,fullPath)
 }


 // 根据沙箱路径下载,这是一个异步过程
  async downloadPDF(fullPath: string): Promise<void> {
    this.controller.startDownload(this.pdfUrl);
    return new Promise((resolve, reject) => {
      try {
        this.delegate.onBeforeDownload((webDownloadItem: webview.WebDownloadItem) => {
          // 传入下载路径,并开始下载。
          webDownloadItem.start(fullPath);
        })
        this.delegate.onDownloadFailed((webDownloadItem: webview.WebDownloadItem) => {
          console.log("download failed guid: " + webDownloadItem.getGuid());
          ToastUtil.showToast('加载失败')
          reject()
        })
        this.delegate.onDownloadFinish((webDownloadItem: webview.WebDownloadItem) => {
          console.log("download finish guid: " + webDownloadItem.getGuid());
          resolve()
        })
        this.controller.setDownloadDelegate(this.delegate);
      } catch (error) {
        console.error(`ErrorCode: ${(error as BusinessError).code},  Message: ${(error as BusinessError).message}`);
        reject()
      }
    })
  }

最后,复制沙箱中的文件到本地Download目录中

 copyToDownload(fileName:string,fullPath:string){
    // 文件选择器配置
    const context = getContext(this) as common.UIAbilityContext;
    const documentViewPicker = new picker.DocumentViewPicker(context);
    const documentSaveOptions = new picker.DocumentSaveOptions();

    documentSaveOptions.newFileNames = [fileName]; // 设置默认文件名
    documentSaveOptions.pickerMode = picker.DocumentPickerMode.DOWNLOAD;

    // 打开文件选择器
    const uris = await documentViewPicker.save(documentSaveOptions);
    if (uris.length > 0) {
      const targetDir = new fileUri.FileUri(uris[0]).path;
      const targetUri = `${targetDir}/${fileName}`;
      // 复制到Download下
      const sourceStats = fs.statSync(fullPath);
      if (sourceStats.isFile()) {
        try {
          fs.copyFileSync(fullPath, targetUri);
          console.info(`文件已保存至:${targetUri}`);
          ToastUtil.showToast('下载成功:' + targetUri)
          // 删除临时文件
          fs.unlinkSync(fullPath);
        } catch (e) {
          console.error('文件复制失败' + e)
          ToastUtil.showToast('下载失败')
        }
      }
    }
 }

通过这种方式,就可以实现pdf的下载和保存,并且无需声明额外的权限。

一招解决 AI 数据格式问题:让 AI 乖乖返回你要的数据结构

作者 叶小秋
2025年4月10日 14:12

一招解决 AI 数据格式问题:让 AI 乖乖返回你要的数据结构

前言

在实际开发中,我们经常需要 AI 生成各种格式的数据。但是 AI 返回的数据格式往往不够规范,需要额外的处理。本文将介绍一个万能方法,让 AI 生成符合预期的数据结构。

核心思路

  1. 使用 Zod 定义数据结构
  2. 将 Zod Schema 转换为 JSON Schema
  3. 在提示词中加入格式说明
  4. 解析并验证 AI 返回的数据

代码实现

首先,我们需要安装必要的依赖:

npm install zod zod-to-json-schema

然后,实现核心代码:

import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';

class StructuredOutputParser<T extends z.ZodTypeAny> {
  schema: T;
  constructor(schema: T) {
    this.schema = schema;
  }
  getFormatInstructions() {
    return `输出应格式化为符合以下 JSON Schema 的 JSON 实例。
    例如,对于 Schema { "properties": { "foo": { "title": "Foo", "description": "a list of strings", "type": "array", "items": { "type": "string" } } }, "required": ["foo"] },
    对象 { "foo": ["bar", "baz"] } 是格式正确的 JSON 实例。
    对象 { "properties": { "foo": ["bar", "baz"] } } 不是 格式正确的 JSON 实例。
    以下是输出的 Schema:
\`\`\`json
${JSON.stringify(zodToJsonSchema(this.schema))}
\`\`\`
`;
  }
  async parse(text: string): Promise<z.infer<T>> {
    try {
      const json = text.includes('```') ? text.trim().split(/```(?:json)?/)[1] : text.trim();

      const escapedJson = json
        .replace(/"([^"\\]*(\\.[^"\\]*)*)"/g, (_match, capturedGroup) => {
          const escapedInsideQuotes = capturedGroup.replace(/\n/g, '\\n');
          return `"${escapedInsideQuotes}"`;
        })
        .replace(/\n/g, '');

      return await this.schema.parseAsync(JSON.parse(escapedJson));
    } catch (e) {
      throw new Error(`Failed to parse. Text: "${text}". Error: ${e}`);
    }
  }
}

class ParameterPrompt {
  private outputParser(schema: z.ZodTypeAny) {
    return new StructuredOutputParser(schema);
  }

  private createZodSchema(value: any): z.ZodTypeAny {
    if (typeof value === 'string') {
      return z.string();
    }
    if (typeof value === 'number') {
      return z.number();
    }
    if (typeof value === 'boolean') {
      return z.boolean();
    }
    if (Array.isArray(value)) {
      if (value.length > 0) {
        return z.array(this.createZodSchema(value[0]));
      }
      return z.array(z.any());
    }
    if (value === null) {
      return z.any().nullable();
    }
    if (typeof value === 'object' && value !== null) {
      return this.createObjectSchema(value);
    }
    return z.any();
  }

  private createObjectSchema(obj: Record<string, any>): z.ZodObject<any> {
    const schemaShape = Object.entries(obj).reduce(
      (acc, [key, value]) => {
        acc[key] = this.createZodSchema(value);
        return acc;
      },
      {} as Record<string, z.ZodTypeAny>,
    );

    return z.object(schemaShape);
  }

  public getPrompt(prompt: string, parameter: object) {
    const generatedZodSchema = this.createObjectSchema(parameter);
    const parser = this.outputParser(generatedZodSchema);
    const formatInstructions = parser.getFormatInstructions();
    const promptTemplate =
      prompt.indexOf('{{parameter}}') !== -1
        ? prompt.replace('{{parameter}}', formatInstructions)
        : prompt + formatInstructions;
    return { prompt: promptTemplate, parser };
  }
}

示例

  1. 生成人物信息
const { prompt, parser } = new ParameterPrompt().getPrompt(
    '请生成一个虚拟人物信息', 
    { 
        name: '名称',
        age: 18,
        description: '描述',
        hobbies: ['爱好']
    }
);

const result = await fetch('AI接口', {
    method: 'POST',
    body: JSON.stringify({ prompt })
});

const data = await parser.parse(result);
// 输出示例:
// {
//   "name": "李四",
//   "age": 25,
//   "description": "一个热爱生活的年轻人",
//   "hobbies": ["阅读", "旅行", "摄影"]
// }
  1. 生成商品数据
const { prompt, parser } = new ParameterPrompt().getPrompt(
    '请生成一个商品信息', 
    {
        id: 1,
        title: '商品名称',
        price: 99.9,
        tags: ['标签'],
        specs: {
            color: '颜色',
            size: '尺寸'
        }
    }
);
const result = await fetch('AI接口', {
    method: 'POST',
    body: JSON.stringify({ prompt })
});
const data = await parser.parse(result);
// 输出示例:
// {
//   "id": 101,
//   "title": "智能蓝牙耳机",
//   "price": 299.99,
//   "tags": ["电子产品", "蓝牙", "无线"],
//   "specs": {
//     "color": "黑色",
//     "size": "标准"
//   }
// }

总结

这个方案通过结合 Zod 的类型系统和自动化的 Schema 生成,提供了一种优雅的方式来处理 AI 生成的结构化数据。它不仅确保了数据格式的准确性,还提高了开发效率。 希望这个方法能帮助大家更好地使用 AI 生成所需的数据结构。如果觉得有帮助,欢迎点赞转发,也欢迎在评论区分享你的想法和建议!

参考资料

React + Antd 实现优雅的应用内搜索组件

作者 yayea
2025年4月10日 14:10

React + Antd 实现优雅的应用内搜索组件

在现代Web应用中,快速导航和搜索功能是提升用户体验的关键要素。本文将介绍一个基于React+Antd的应用内搜索组件AppSearch,它提供了类似VSCode命令面板的体验,使用户能够通过关键词快速搜索并导航到应用内不同页面。

image.png

组件架构

整个搜索组件由以下部分组成:

  1. AppSearch: 主组件,渲染搜索图标和触发搜索模态框
  2. AppSearchModal: 搜索模态框,包含搜索输入框和结果列表
  3. AppSearchFooter: 显示快捷键提示
  4. AppSearchKeyItem: 用于渲染快捷键图标

同时,还包含几个自定义Hook:

  • useMenuSearch: 处理菜单搜索逻辑
  • useClickOutside: 处理点击外部关闭模态框
  • useRefs: 管理多个React引用

主组件实现

import React, { useState } from 'react';
import { Tooltip } from 'antd';
import { createStyles } from 'antd-style';
import { Icon } from '@iconify/react';
import AppSearchModal from './components/AppSearchModal';

// 使用antd-style创建样式
const useStyles = createStyles(({}) => ({
  search: {
    padding: '0 8px',
    cursor: 'pointer',
  },
}));

const AppSearch: React.FC = () => {
  const { styles } = useStyles();
  const [showModal, setShowModal] = useState<boolean>(false);

  return (
    <div className={styles.search} onClick={() => setShowModal(true)}>
      <Tooltip title="搜索">
        <Icon icon="ant-design:search-outlined" />
      </Tooltip>
      <AppSearchModal visible={showModal} onClose={() => setShowModal(false)} />
    </div>
  );
};

export default AppSearch;

主组件实现十分简洁,通过useState管理模态框的显示状态,点击搜索图标时打开模态框。

搜索模态框组件

搜索模态框是核心交互界面,它使用createPortal将组件渲染到DOM树的其他位置(本例中是document.body),这样可以避免父组件样式对模态框的影响。

// 模态框组件核心结构
const AppSearchModal: React.FC<AppSearchModalProps> = ({ visible, onClose }) => {
  const { styles, cx } = useStyles();
  const modalRef = useRef<HTMLDivElement>(null);
  const scrollRef = useRef<HTMLUListElement>(null);
  const [itemRefs, setItemRefs] = useRefs<HTMLLIElement>();

  // 使用自定义hook处理搜索逻辑
  const { handleSearch, searchResult, keyword, activeIndex, handleEnter, handleMouseEnter } =
    useMenuSearch({
      itemRefs,
      scrollRef,
      onClose,
    });

  // 关闭模态框并清空搜索结果
  const handleClose = () => {
    onClose();
    searchResult.length = 0;
  };

  // 点击外部关闭模态框
  useClickOutside(modalRef, handleClose);

  const isNotData = !keyword || searchResult.length === 0;

  if (!visible) return null;

  const modalContent = (
    <div className={styles.mask} onClick={(e) => e.stopPropagation()}>
      <div className={styles.modal} ref={modalRef}>
        {/* 搜索输入框 */}
        <div className={styles.inputWrapper}>
          <Input
            classNames={{ input: styles.input }}
            placeholder="搜索"
            autoFocus
            allowClear
            onChange={handleSearch}
            prefix={<Icon icon="ant-design:search-outlined" />}
          />
          <span className={styles.cancel} onClick={handleClose}>
            取消
          </span>
        </div>

        {/* 搜索结果区域 */}
        {isNotData && <div className={styles.notData}>没有找到相关结果</div>}

        {!isNotData && (
          <ul className={styles.list} ref={scrollRef}>
            {searchResult.map((item, index) => (
              <li
                key={item.path}
                ref={setItemRefs(index)}
                data-index={index}
                onMouseEnter={handleMouseEnter}
                onClick={handleEnter}
                className={cx(styles.item, {
                  [styles.itemActive]: activeIndex === index,
                })}
              >
                {item.icon && (
                  <div className={styles.itemIcon}>
                    <Icon icon={item.icon} />
                  </div>
                )}
                <div className={styles.itemText}>{item.name}</div>
                <div className={styles.itemEnter} data-role="item-enter">
                  <Icon icon="ant-design:enter-outlined" />
                </div>
              </li>
            ))}
          </ul>
        )}

        <AppSearchFooter />
      </div>
    </div>
  );

  // 使用createPortal将模态框渲染到body
  return createPortal(modalContent, document.body);
};

搜索逻辑的核心Hook - useMenuSearch

useMenuSearch是搜索功能的核心,它实现了以下功能:

  1. 菜单数据的获取和处理
  2. 关键词搜索
  3. 键盘导航(上下箭头、回车、ESC等)
  4. 结果高亮与滚动定位
export function useMenuSearch({ itemRefs, scrollRef, onClose }: UseMenuSearchProps) {
  const [keyword, setKeyword] = useState('');
  const [searchResult, setSearchResult] = useState<SearchResult[]>([]);
  const [activeIndex, setActiveIndex] = useState(-1);
  const menuListRef = useRef<Menu[]>([]);

  // 初始化菜单数据
  useEffect(() => {
    initMenu();
  }, []);

  /**
   * 处理菜单树搜索,返回搜索结果列表
   * @param menuTree 菜单树
   * @param reg 搜索正则
   * @param parent 父菜单项(用于构建层级名称)
   * @returns 搜索结果列表
   */
  const searchMenu = useCallback((menuTree: Menu[], reg: RegExp, parent?: Menu): SearchResult[] => {
    const results: SearchResult[] = [];

    menuTree.forEach((menu) => {
      const { name, path, icon, children, hideMenu, meta, internalOrExternal } = menu;
      const hasChildren = Array.isArray(children) && children.length > 0;
      const hideChildren = meta?.hideChildrenInMenu;

      // 判断当前菜单是否匹配搜索条件
      const isMatch = !hideMenu && reg.test(name);

      // 如果菜单匹配且是叶子节点或配置了隐藏子菜单,则添加到结果中
      if (isMatch && (!hasChildren || hideChildren)) {
        results.push({
          name: parent?.name ? `${parent.name} > ${name}` : name,
          path,
          icon,
          internalOrExternal,
        });
      }

      // 处理子菜单
      if (hasChildren && !hideChildren) {
        results.push(...searchMenu(children, reg, menu));
      }
    });

    return results;
  }, []);

  // 搜索处理,使用debounce防抖优化
  const handleSearch = useCallback(
    debounce((e: React.ChangeEvent<HTMLInputElement>) => {
      const key = e.target.value;
      setKeyword(key.trim());
      if (!key) {
        setSearchResult([]);
        return;
      }

      const reg = createSearchReg(key);
      setSearchResult(searchMenu(menuListRef.current, reg));
      setActiveIndex(0);
    }, 200),
    [searchMenu],
  );
  
  // 搜索结果导航与键盘事件处理
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      switch (e.key) {
        case 'Enter':
          handleEnter();
          break;
        case 'ArrowUp':
          handleUp();
          break;
        case 'ArrowDown':
          handleDown();
          break;
        case 'Escape':
          handleClose();
          break;
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [handleEnter, handleUp, handleDown, handleClose]);

  return {
    handleSearch,
    searchResult,
    keyword,
    activeIndex,
    handleMouseEnter,
    handleEnter,
  };
}

辅助Hook介绍

useClickOutside - 点击外部区域监听

/**
 * 监听点击元素外部的事件
 * @param ref 目标元素的ref
 * @param handler 点击外部时的回调函数
 * @param enabled 是否启用该hook,默认为true
 */
export const useClickOutside = (
  ref: RefObject<HTMLElement>,
  handler: () => void,
  enabled = true,
) => {
  useEffect(() => {
    if (!enabled) return;

    const debouncedHandler = debounce((event: MouseEvent | TouchEvent) => {
      if (!ref.current || ref.current.contains(event.target as Node)) return;
      handler();
    }, 100);

    document.body.addEventListener('mousedown', debouncedHandler);
    document.body.addEventListener('touchstart', debouncedHandler);

    return () => {
      document.body.removeEventListener('mousedown', debouncedHandler);
      document.body.removeEventListener('touchstart', debouncedHandler);
      debouncedHandler.cancel();
    };
  }, [ref, handler, enabled]);
};

useRefs - 管理多个元素引用

/**
 * 管理多个元素的refs
 * @returns [refs数组, 设置ref的函数]
 */
export function useRefs<T extends HTMLElement = HTMLElement>(): UseRefsReturn<T> {
  const refs = useRef<T[]>([]);

  useEffect(() => {
    refs.current = [];
  });

  const setRefs = useCallback(
    (index: number) => (instance: T) => {
      refs.current[index] = instance;
    },
    [],
  );

  return [refs, setRefs];
}

底部快捷键提示组件

export const AppSearchFooter: React.FC = () => {
  const { styles } = useStyles();

  return (
    <div className={styles.footer}>
      <AppSearchKeyItem className={styles.item} icon="ant-design:enter-outlined" />
      <span>搜索</span>

      <AppSearchKeyItem className={styles.item} icon="ant-design:arrow-up-outlined" />
      <AppSearchKeyItem className={styles.item} icon="ant-design:arrow-down-outlined" />
      <span>导航</span>

      <AppSearchKeyItem className={styles.item} icon="mdi:keyboard-esc" />
      <span>关闭</span>
    </div>
  );
};

技术亮点解析

Iconify 图标解决方案

Iconify 是一个强大的图标解决方案,它提供了以下优势:

  1. 统一API: 通过统一的API访问多个图标集(包括Material Design, Ant Design等)
  2. 按需加载: 只加载实际使用的图标,减少体积
  3. 无需管理图标资源: 无需下载和维护SVG文件

使用示例:

import { Icon } from '@iconify/react';

// 使用Ant Design图标
<Icon icon="ant-design:search-outlined" />

// 使用Material Design图标
<Icon icon="mdi:keyboard-esc" />

antd-style 样式解决方案

antd-style 是Ant Design的新一代样式解决方案,它提供了:

  1. 主题变量访问: 直接访问Ant Design的主题变量
  2. TypeScript支持: 完全类型化的API
  3. CSS-in-JS: 组织和隔离组件样式

使用示例:

import { createStyles } from 'antd-style';

const useStyles = createStyles(({ token }) => ({
  container: {
    color: token.colorPrimary,
    backgroundColor: token.colorBgContainer,
    borderRadius: token.borderRadiusLG,
  }
}));

const Component = () => {
  const { styles, cx } = useStyles();
  
  return <div className={styles.container}>内容</div>;
};

可能存在的问题

在使用createStyles创建模态框样式时,由于编译后的样式类名发生了变化,所以如果使用以下方式,并不能选中.item-enter元素(选中菜单项尾部的enter图标)

  itemActive: {
    color: token.colorTextLightSolid,
    backgroundColor: token.colorPrimary,
    '.item-enter': {
      opacity: 1,
    },
  }
  itemEnter: {
    width: '30px',
    opacity: 0,
  },

因此,采用了以下方式来实现:

  itemActive: {
    color: token.colorTextLightSolid,
    backgroundColor: token.colorPrimary,
    '& [data-role="item-enter"]': {
      opacity: 1,
    },
  }
  itemEnter: {
    width: '30px',
    opacity: 0,
  },
  
  <div className={styles.itemEnter} data-role="item-enter">
    <Icon icon="ant-design:enter-outlined" />
  </div>

其中:

  • & 表示当前选择器的引用
  • [data-role="item-enter"] 是一个属性选择器,选择带有特定data属性的元素

PS: 暂时未找到createStyles关于此类嵌套样式的直接解决方案,有更好的实现方式也可以评论区交流一下

总结

通过这个搜索组件,我们实现了一个类似VS Code的命令面板功能,它提供了以下特性:

  1. 友好的UI: 简洁直观的搜索界面
  2. 键盘导航: 支持键盘快捷键,提高使用效率
  3. 模糊搜索: 支持模糊匹配菜单项
  4. 响应式设计: 适配移动端和桌面端

这个组件展示了如何使用 React + Antd 实现优雅的应用内搜索组件,通过合理的架构设计和组件拆分,实现了复杂功能的简洁实现。

希望这篇文章对你有所启发,欢迎在评论区分享你的想法和改进建议!

即刻体验!文心大模型X1现面向企业用户全面开放!

作者 百度Geek说
2025年4月10日 11:24

4月2日,文心大模型X1正式上线百度智能云千帆大模型平台,企业用户和开发者登录即可调用API。

文心大模型X1具备更强的理解、规划、反思、进化能力,并支持多模态,是能力更全面的深度思考模型。模型兼备准确、创意和文采,在中文知识问答、文学创作、文稿写作、日常对话、逻辑推理、复杂计算及工具调用等方面表现尤为出色。

据权威测试,在多个公开数据集测评中,文心大模型X1在数学、代码、知识推理等能力上表现优异,超越升级后的DeepSeek-V3-0324。在数学场景中,GSM8K数据集测试后结果显示,文心X1得分95.6,DeepSeek-V3-0324得分93.6;代码生成层面,MBPP数据集测试后结果显示,文心X1得分83.3,DeepSeek-V3-0324得分81.2;在知识推理层面,C-Eval数据集测试后结果显示,文心X1得分88.6,DeepSeek-V3-0324得分85.1;在数学推理层面,Aime2024数据集测试后结果显示,文心X1得分78.6,DeepSeek-V3-0324得分57.1。

图片

在模型服务方面,现可直接通过千帆ModelBuilder调用文心大模型X1 API服务,输入价格低至0.002元/千tokens,输出价格低至0.008元/千tokens,同时支持批量推理场景,满足更多元业务需求。

在模型开发方面,千帆ModelBuilder模型蒸馏支持文心大模型X1作为教师模型,一键实现从“思考模型”到“轻量模型”的知识蒸馏。

在应用开发方面,千帆AppBuilder支持用户基于文心大模型X1,快速搭建具有深思考能力的RAG、Agent、AI搜索、工作流,实现更充分的思考规划、更准确的工具调用、更全面的生成效果,助力企业级大模型应用在千行百业落地。

百度智能云千帆大模型平台始终致力于为用户提供全流程、一站式的AI服务,以开放性、易用性、低成本的平台理念,企业用户和开发者能够更高效地探索大模型应用,提升创新效率,加速各类AI应用从概念到落地的转化,为AI技术在更多领域的拓展与应用注入强大动力。

------END------

推荐阅读

一篇论文,看见百度广告推荐系统在大模型时代的革新

前沿多模态模型开发与应用实战3:DeepSeek-VL2多模态理解大模型算法解析与功能抢先体验

秒哒首发即爆发!上线首日吸引2万用户,打造3万应用!

秒哒,全面开放!

图灵数据洞察平台-TDF(Turing Data Finder)

一个脚本让任何框架无痛实现 SSR,无需转到 Next.js、Nuxt.js

作者 寅时码
2025年4月9日 16:33

SPA 静态化实战:无需替换框架,低成本地将 React、Vue... 应用转为 SSG 的方案

SPA 的甜蜜烦恼与性能优化的诉求

单页面应用(SPA)以其流畅的客户端路由和丰富的交互体验赢得了开发者的青睐。然而,这种架构也带来了一些挑战,尤其是在性能和爬虫可见性方面:

  1. 搜索引擎优化(SEO): 尽管搜索引擎爬虫能力在提升,但它们抓取和理解依赖 JavaScript 动态渲染内容的 SPA 页面,仍不如直接处理静态 HTML 那样高效和稳定。这可能影响网站在搜索结果中的排名,对内容驱动型网站尤其不利。
  2. 首屏加载性能(FCP/LCP): SPA 首次加载需下载并执行大量 JavaScript,然后才能渲染内容和请求数据。这个过程可能导致较长的白屏时间,影响用户体验,特别是在网络或设备性能受限时。

主流 SSR 方案的挑战与迁移成本

为了解决上述问题,社区涌现了像 Next.js (基于 React) 和 Nuxt.js (基于 Vue) 这样的优秀服务端渲染(SSR)或静态站点生成(SSG)框架。它们提供了开箱即用的解决方案,能有效改善 SEO 和首屏性能。

然而,将现有的、基于标准 React/Vue CLI 构建的 SPA 项目迁移到这些框架,并非总是轻松之举:

  • 学习成本: 需要学习新框架的约定、路由机制、数据获取方式(如 getServerSideProps, getStaticProps, asyncData 等)、配置和生命周期。
  • 项目重构: 往往需要对现有代码结构、路由逻辑、状态管理、甚至是 CSS 处理方式进行较大规模的调整,以适应框架的规范。这对于已具规模或历史悠久的项目来说,可能意味着巨大的工作量和潜在风险。
  • 构建与部署复杂性: SSR 方案通常需要运行一个 Node.js 服务器环境,对部署架构有特定要求。虽然它们也支持 SSG 模式,但整个开发和构建流程与传统 SPA 不同。

那么,有没有一种方式,可以在不颠覆现有 SPA 项目结构的前提下,低成本地享受 SSG 带来的好处呢?

这就是我开发这个小工具的初衷:提供一种非侵入式的、轻量级的解决方案,让你能够为现有 SPA 项目中的特定页面(如首页、关于页、产品页等)生成静态 HTML,从而快速优化这些关键页面的 SEO 和加载性能,而无需进行大规模的项目迁移或重构。

github.com/beixiyo/ssg…

使用示例

安装

npm i @jl-org/ssg puppeteer-core -D

使用

  1. 生成静态 HTML
const path = require('node:path')
const { ssg } = require('@jl-org/ssg')
const puppeteer = require('puppeteer-core')

const PORT = '4173'
main()

async function main() {
  await ssg({
    port: PORT,
    /**
     * 创建浏览器实例的函数,默认需要你自行下载 puppeteer-core 或者 puppeteer
     * ### 因为 puppeteer 会自动下载浏览器,所以提供此配置。你可以使用自己浏览器的路径,节省内存
     * @example
     * puppeteer.launch({ headless: true, ... })
     */
    createBrowser: () => puppeteer.launch({
      executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe',
      headless: true
    }),
    ssgPages: [
      {
        url: `http://localhost:${PORT}`,
        target: path.resolve(__dirname, '../dist/index.html')
      },
      {
        url: `http://localhost:${PORT}/pricing`,
        target: path.resolve(__dirname, '../dist/pricing.html')
      },
      {
        url: `http://localhost:${PORT}/about`,
        target: path.resolve(__dirname, '../dist/about.html')
      },
    ]
  })

  process.exit(0)
}
  1. 修改 Nginx 配置,并且重启服务
server {
  listen 80;
  server_name yourdomain.com;

  # pages
  location /pricing {
    root /Your_Root_Dir;
    index pricing.html;
    try_files $uri $uri/ /pricing.html;
  }
  location /about {
    root /Your_Root_Dir;
    index about.html;
    try_files $uri $uri/ /about.html;
  }
}
  1. 测试生效
# 查看返回内容,是否为整个 HTML
curl http://yourdomain.com/pricing

核心原理与工作流程

这个工具的核心思路是在构建流程中,模拟真实用户访问应用的过程,捕获最终渲染好的 HTML,并将其保存为静态文件。

以下是详细的工作流程及关键代码原理:

  1. 启动本地预览服务器:

    • 动作: 执行命令(如 npx vite preview --port <端口>)启动 SPA 的预览服务。默认使用 vite,你可自定义命令
    • 原理: 需要一个 HTTP 服务器来提供 SPA 资源。我们使用 Node.js 的 child_process.spawn 异步启动这个服务。
      /** 伪代码示意 */
      const proc = spawn('npx', ['vite', 'preview', '--port', '4173'], { shell: true })
      /** 通过监听 stdout 确认服务器启动成功 */
      proc.stdout.on('data', (data) => {
        if (data.toString().includes('server running at')) {
          resolve(killServerFunction) /** 服务器就绪,继续下一步 */
        }
      })
      
    • 关键: 确保服务器完全启动后再进行下一步。
  2. 启动无头浏览器实例:

    • 动作: 利用 Puppeteer 库启动一个无头(headless)浏览器实例。
    • 原理: 无头浏览器提供 API 控制浏览器行为。通过 createBrowser 配置,允许用户指定本地浏览器路径,避免额外下载。
    • 注意: 为了节省用户内存,启动浏览器需要用户从外部传入,你可以选择下载 puppeteer-core,传入自己浏览器路径来节省内存;或者下载 puppeteer,直接用自动下载的浏览器
      const puppeteer = require('puppeteer-core')
      
      /** 伪代码示意 - createBrowser 函数内 */
      const browser = await puppeteer.launch({
        executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe', /** 用户指定的路径 */
        headless: true /** 在后台运行 */
      })
      return browser
      
    • 关键: 使用 headless: true 在后台执行,executablePath 提供灵活性。
  3. 访问目标页面:

    • 动作: 无头浏览器导航到配置中指定的每个本地 URL(如 http://localhost:4173/about)。
    • 原理: 调用 Puppeteer Page 对象的 goto 方法。
      /** 伪代码示意 - genHtml 函数内 */
      const page = await browser.newPage()
      await page.goto('http://localhost:4173/about', { /* ... */ })
      
  4. 等待页面完全加载:

    • 动作: 等待页面资源加载完毕且网络活动静默。
    • 原理: 使用 goto 方法的 waitUntil: 'networkidle0' 选项。这指示 Puppeteer 等待,直到在 500ms 内网络连接数少于或等于 0,通常意味着异步加载和渲染已完成。
      /** 伪代码示意 - page.goto 调用时 */
      await page.goto(url, {
        waitUntil: 'networkidle0', /** 等待网络空闲 */
        timeout: 100000 /** 设置较长超时以防页面加载慢 */
      })
      
    • 关键: networkidle0 是捕获完整动态内容的核心。
  5. 捕获 HTML 内容:

    • 动作: 获取当前页面的完整 DOM 结构,并序列化为 HTML 字符串。
    • 原理: 调用 Page 对象的 content() 方法。
      /** 伪代码示意 - genHtml 函数内 */
      const html = await page.content()
      
  6. (可选)HTML 优化与压缩:

    • 动作: 若启用 needMinify,使用 html-minifier-terser 库压缩 HTML。
    • 原理: 调用 minify 函数,传入 HTML 内容和压缩选项。
      /** 伪代码示意 - genHtml 函数内 */
      if (needMinify) {
        html = await minify(html, {
          collapseWhitespace: true,
          removeComments: true,
          // ... 其他压缩选项
        })
      }
      
    • 关键: 减小文件体积,提升加载速度。
  7. 路径处理:

    • 原因: 通过开发服务器启动的页面,里面的资源是 https://localhost:PORT/xxx.png 之类的路径,这在服务器里不可用,应该替换掉 http 路径,移除 HTML 中指向本地预览服务器的绝对 URL 前缀。
    • 原理: 使用字符串的 replace 方法配合正则表达式,将 http://localhost:PORT 替换为空字符串,使资源引用变为相对路径或根相对路径。
      /** 伪代码示意 - genHtml 函数内 */
      const curUrl = `http://localhost:${PORT}` /** 获取当前基础 URL */
      html = html.replace(new RegExp(curUrl, 'g'), '') /** 全局替换 */
      
    • 关键: 保证部署后的资源路径正确。
  8. 写入静态文件:

    • 动作: 将处理后的 HTML 内容写入指定的文件路径。
    • 原理: 使用 Node.js 的 fs.writeFileSync
      /** 伪代码示意 - genHtml 函数内 */
      writeFileSync(targetPath, html) // targetPath 是配置中指定的输出路径
      
  9. 循环与清理:

    • 动作: 对所有配置的页面完成 SSG 后,关闭浏览器页面和实例,并终止预览服务器进程。
    • 原理: 使用 page.close() 关闭单个页面,browser.close() 关闭浏览器。最后还需要关闭命令行(npx vite preview),使用 tree-kill 库确保预览服务器及其所有子进程被彻底关闭,释放资源。
      import treeKill from 'tree-kill'
      
      treeKill(pid, 'SIGTERM', (err) => {
        if (err) {
          console.error(`关闭服务器失败 ${pid}:`, err)
        }
        else {
          console.log(`Process ${pid} 已经关闭`)
        }
      })
      
    • 关键: 资源管理,避免进程残留。

记一次为js库开发声明文件的过程

作者 bug修士
2025年4月9日 11:37

背景

前段时间同事给我推了font-carrier 这个字体库,让我去开发一些字体工具。在使用这个库的过程中,发现这个库使用JavaScript开放,而我的项目用的是TypeScript,开发体验非常差。于是,我就萌生了为这个库添加类型的想法。

最后通过调研决定通过DefinitelyTyped实现。

什么是DefinitelyTyped

简单来说就是为那些使用纯js编写的库添加typescript类型的一种方式,例如React对应的@types/react。

所以为了font-carrier支持类型,就需要@types/font-carrier的声明库。

实现步骤

1 fork仓库

首先我们需要fork一份DefinitelyTyped的仓库,这样才能随意更改,当改完后再给他们提PR即可。

仓库地址:github.com/DefinitelyT…

2 拉取fork后的仓库

git clone git@github.com:zFitness/DefinitelyTyped.git

3 安装依赖

cd DefinitelyTyped
yarn

4 dts-gen安装

npm i -g dts-gen

5 使用dts-gen创建目录

npx dts-gen --dt --name font-carrier --template module

6 编写类型

这里有个注意事项,如果你的库是使用commonjs语法的导入导出,而不是es module的导入导出。

export = test
import test = require('')

参考文档: www.typescriptlang.org/docs/handbo…

下面是一些常用的写法转换:

导出一个对象

commonjs 导出一个对象时候:

const maxInterval = 12;
function getArrayLength(arr) {
  return arr.length;
}
module.exports = {
  getArrayLength,
  maxInterval,
};

写成ts:

export function getArrayLength(arr: any[]): number;
export const maxInterval: 12;

导出正则表达式

module.exports = /hello/

ts可以写成这样子

declare const helloworld: RegExp;
export default helloworld;

导出数字

module.exports = 3.14

ts 可以写成如下:

declare const PI:  number;
export default PI;

导出函数

CommonJS 中的一种导出方式是导出函数。因为函数也是对象,所以可以添加额外的字段并将其包含在导出中:

function getArrayLength(arr) {
  return arr.length;
}
getArrayLength.maxInterval = 12;
module.exports = getArrayLength

转换成ts如下:

export default function(arr: any[]): number;
export const maxInterval: 12;

注意必须在 d.ts 中开启 esModuleInterop: true, 例如提交PR到Definitely Typed中的时候。这里可以使用export=来代替

declare namespace getArrayLength {
  declare const maxInterval: 12;
}
declare function getArrayLength(arr: any[]): number;
export = getArrayLength;

7 发布测试

开发完后可以通过test命令进行测试。

npm test font-carrier

测试流程通过就可以提交代码,再提交一个PR,对方审核通过就会自动发布。

下面是我的提交PR:

github.com/DefinitelyT…

最后关于我编写的@types/font-carrier, 欢迎使用: www.npmjs.com/package/@ty…

参考资料

❌
❌