普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月6日首页

商务部电子商务司负责人解读《关于更好服务实体经济 推进电子商务高质量发展的指导意见》

2026年4月6日 12:35
36氪获悉,近日,商务部会同中央网信办、工业和信息化部、农业农村部、文化和旅游部、市场监管总局等5部门印发《关于更好服务实体经济推进电子商务高质量发展的指导意见》(下称《意见》),商务部电子商务司负责人就《意见》进行了解读。

告别 `any`:TypeScript 中 `try...catch` 的最佳实践

作者 ssshooter
2026年4月6日 12:42

在 TypeScript 项目中,你是否经常为了通过编译而写出这种代码?

try {
  // 某些逻辑
} catch (err: any) { // ❌ 违背了 TS 类型安全的初衷
  console.log(err.message); 
}

随着 TS 配置趋于严格,catch(err: any) 往往会触发 ESLint 警告或编译错误。本文将介绍处理 catch 块中错误对象的几种最佳实践

1. 理解 unknown 的必然性

在现代 TypeScript(4.0+)中,推荐将捕获到的错误声明为 unknown。这是因为在运行时刻,你无法保证捕获到的一定是 Error 实例。

try {
  throw "意外的错误字符串"; // 这里的错误甚至不是一个对象
} catch (err: unknown) {
  // ❌ 报错:'err' is of type 'unknown'
  // console.log(err.message); 
}

2. 方案一:类型守卫(Type Guards)—— 最稳健的方法

这是官方推荐的做法。通过显式的 instanceof 检查,TS 会在代码块内自动收窄(Narrowing)类型。

try {
  await fetchData();
} catch (err: unknown) {
  if (err instanceof Error) {
    // ✅ TS 现在知道 err 是 Error 类型
    console.error(err.message);
    console.error(err.stack);
  } else {
    // 处理非标准错误(如 throw "string")
    console.error("发生了未知类型的错误", err);
  }
}

3. 方案二:自定义工具函数(封装大法)

如果你觉得到处写 if (err instanceof Error) 太麻烦,可以封装一个工具函数。这是目前大型项目中最流行的做法。

编写工具函数

function toError(err: unknown): Error {
  if (err instanceof Error) return err;
  return new Error(String(err));
}

业务中使用

try {
  doSomething();
} catch (err: unknown) {
  const error = toError(err);
  console.log(error.message); // ✅ 永远安全
}

4. 方案三:函数式处理(类似 Rust/Go)

如果你讨厌深层嵌套的 try...catch,可以使用封装好的包装器,将错误作为返回值返回。

async function safeRun<T>(promise: Promise<T>): Promise<[Error | null, T | null]> {
  try {
    const data = await promise;
    return [null, data];
  } catch (err: unknown) {
    return [toError(err), null];
  }
}

// 使用:
const [err, data] = await safeRun(fetchUser(id));
if (err) {
  handle(err);
} else {
  render(data);
}

5. 进阶:处理 Axios 等库的特定错误

如果你在使用 Axios,可以使用它内置的类型守卫:

import axios from 'axios';

try {
  await axios.get('/api/user');
} catch (err: unknown) {
  if (axios.isAxiosError(err)) {
    // 这里可以访问 err.response, err.status 等特有属性
    console.log(err.response?.data);
  }
}

总结:该选哪一个?

场景 推荐做法
临时处理/小型脚本 if (err instanceof Error)
标准大型项目 封装 toError() 工具函数,确保类型安全
追求代码扁平化 采用 safeRun 包装器返回 [err, data]
第三方库请求 优先使用库提供的 isError 判断函数

核心原则: 永远不要相信 catch 捕获到的内容,永远在访问属性前进行类型检查。这不仅是过编译的要求,更是写出健壮代码的基石。

红果短剧:针对近期AI短剧素材违规使用问题频发的情况,已处置作品670部

2026年4月6日 12:24
红果短剧官方账号4月6日发布《关于持续治理AI短剧素材违规使用行为的公告》:今年第一季度,平台已累计下架违反平台治理规范的漫剧1718部。其中针对近期AI短剧素材违规使用问题频发的情况,平台专项开展集中治理。目前已完成1.5万部作品的全面核查,依规处置违规作品670部。(界面)

清明假期迎返程高峰 三天假期跨区域人员流动量预计达8.4亿人次

2026年4月6日 12:15
今天是清明假期的最后一天,全国出行迎来返程高峰,全社会跨区域人员流动量增长明显。交通运输部数据显示,三天假期,全社会跨区域人员流动量预计达8.4亿人次,铁路、公路、民航首日客运量均超历史同期峰值。 今天,全国铁路预计发送旅客2080万人次,计划加开旅客列车1369列。(证券时报)

众筹300万美元的Agent盒子,想彻底解决你的算力焦虑

2026年4月6日 11:53

作者 | 张子怡

编辑 | 袁斯来

AI硬件赛道,似乎一夜之间跨入新世界。

上一个月,投资人还在打听硬件大厂高P创始人,这一个月,他们已经开始寻觅“下一个Mac mini”。

狂欢的起点,来自开源智能体框架OpenClaw(圈子里称为:龙虾)。从抢购大内存的Mac mini到各类软硬大厂的联名主机,FOMO恐惧症席卷着所有人。承载AI的硬件,如今成了不可错失的入口。

此前更多在极客圈子里风行的Agent Box忽然被推到前台。简单说,Agent Box就是⼀台面向个人用户的AI专用设备,其存在的唯⼀目的是在本地运行大模型和自主代理。

市面上已经有数家推出Agent Box的公司,包括Pamir、Violoop、Tiiny等等。Pamir估值超过2500万美元,而Tiiny AI推出的首款产品Tiiny AI Pocket Lab,在Kickstarter上众筹金额已达280万美金。有行业人士预计其最终众筹金额可能会超400万美金。

Tiiny AI Pocket Lab重量约300克,尺寸约一部手机大小,早鸟众筹价为1399美元,支持一键部署大模型(最高支持120B)不依赖云端、服务器或高端GPU,也不产生额外的Token消耗费用。

Tiiny AI无疑踩中风口,毕竟用户只需要花单次硬件的钱,便能无限量地使用“龙虾”。

不过,Tiiny AI副总裁兼商业化负责人Eco Lee在采访中曾反复强调:Tiiny AI Pocket Lab不是专门为Open claw设计,它是为个人设计的AI基础设施设备。

这听起来很迷人,甚至有些难以置信。人们必须展开新的想象,agent原生意味着什么,要实现什么?当制约我们使用AI的只剩下高昂的Token费用,我们又如何冲破这一限制?

Tiiny AI尝试给出一个答案。

01 何谓Agent Box

在理解Tiiny AI之前,需要厘清一个新的产品概念——到底什么是Agent Box(智能体盒子)?

在过去一年里,为了在本地运行开源大模型,人们们尝试过各种方案:有人用淘汰的旧电脑,有人抢购顶配的Mac mini。

这笔硬投入相当高昂。倘若用户想在本地端运行超过120B的大模型,购买PC电脑要凑近80GB的显存,整机成本超过5万元;即便选择苹果的Mac Studio(选配96GB统一内存版),也要花费超过2万元。

“你是否愿意买台电脑,只用来跑大模型?现在很多几万元的AI电脑,一旦启动本地大模型的时候,内存与算力就被过度占用,你甚至连一个网页都打不开。更别提打游戏或者看视频。”Eco说到。

除了设备本身开销之外,随着Token价格水涨船高,高昂的持续使用成本,也让“本地化部署”成为行业刚需。

因此,在Tiiny AI的产品构想里,其产品必须是台专门的AI设备,能够支持本地大模型和智能体7×24小时后台运行。其设计逻辑并非替代用户的个人电脑,而是作为外接独立设备,供手机、PC、平板或机器人等终端设备接入调用。系统默认将用户数据、凭证和工作流保存在本地,敏感操作无需上传云端,除非明确要求调用更强的云端模型。

在软件生态方面,设备将内置Agent Store,目前已适配OpenAI OSS、Qwen、GLM 等50余款开源大模型,以及 OpenClaw、n8n 等超百款智能体开发工具。

为构建丰富的端侧生态,Tiiny AI计划于今年7月推出模型格式转换工具,除了Tiiny官方支持的SOTA开源模型外,用户也可自行从Hugging Face等开源社区下载、转换并导入其他开源模型和用户自己的微调模型,并能上传分享给其他Tiiny用户。

“我经常给用户打个比方,云端的大模型就像瓶装矿泉水,好喝也要喝,但普通用户有大量高频、重复、贴近个人习惯、又不需要顶级智力的AI需求,就像用矿泉水洗手洗澡就太奢侈了。Tiiny的存在就像属于用户的‘AI水龙头’,你可以随意使用、token边际成本为0。”Eco告诉硬氪。

在Eco看来,云端大模型专注处理高智力、高精度、关键性任务,本地大模型则聚焦日常高频、个性化、带用户长期记忆的持续交互场景——这种“端云协同”模式,正是Tiiny AI,也是Agent Box最核心的价值所在。

在期待、掌声与纷至沓来的投资人邀约中,Tiiny AI难免要面对质疑。它必须回答的第一个问题,就是以并不昂贵的售价,如何实现前沿的产品理念,以及120B的参数模型?

02 是玩具还是工具

在海外reddit论坛上,对Tiiny AI Pocket Lab的评价两极分化。有人说这只会是玩具,甚至有人通过宣传照逆向工程了Tiiny AI的产品,认为其所宣称的功能很难真正实现。

他们质疑的点在于,Tiiny AI并未公布其所用的SoC(系统芯片)品牌,也没有使用高端的GPU,却能在本地运行120B的大模型。

这令人觉得不可思议。

“我们是一家AI Infra公司,核心是通过系统性底层优化,把有限硬件的每一分算力与资源,全部聚焦于LLM推理与Agent运行,这与其他做硬件的思路有本质不同。”Eco表示。

Tiiny AI Pocket Lab里使用的芯片是一颗SoC外加一颗dNPU,并通过Tiiny AI最核心的技术PowerInfer来实现媲美Nvidia、AMD等高端GPU芯片的本地模型推理能力。

PowerInfer是用于端侧的异构算力推理加速技术,Tiiny AI团队通过大量的数据测算和语料训练发现,大模型推理过程中,参数激活模式分为两类:“热激活参数”(每次与模型交互都会调用的核心参数,约占20%)和“冷激活参数”(仅在用户问到医学、法律等特定领域问题时激活,约占80%)。这种冷热激活的特性,恰好适合在端侧异构算力架构下优化分配。团队开源过PowerInfer的示例:用单个NVIDIA RTX 4090 GPU,运行参数量175B的大模型,速度能达到传统方案的11倍。

这些都属于AI Infra层面的技术积累。从芯片层到Agent调度层,再到模型训练层,都需要深厚的knowhow支撑。

在具体的落地场景中,Tiinny AI团队从Kickstarter的留言区中发现,其用户主要是使用如OpenClaw一类开源应用的普通用户、对数据隐私有刚需的专业人士和AI极客。即使在断网的离线环境下,该设备依然可以运行多步推理、Agent工作流、内容生成以及针对敏感数据的安全计算。“傻瓜式”的开箱即用、0token费的24/7 Agent和完全的自主控制权是这些人选择Tiiny的核心理由。

并且,该设备在系统内引入了长期记忆功能。用户的交互偏好、历史文件与对话记录,均能通过加密形式直接存储在本地硬件中。

“隐私是Agent Box的加分项,但核心在于本地模型的部署,它能够结合你的长期记忆主动做事情,这件事最重要。”Eco告诉硬氪。

Tiiny AI的产品预计在2026年8月交付。需要注意,Tiiny是AI Agent出身的团队,其最终产品呈现仍然是硬件。他们有合作供应商,但也需要应对量产中的突发情况。

硬件生产有太多需要趟过的河流,考验团队的并非融资,而是真正兑现诱人的承诺:在一个300克的盒子上,实现本地算力自由、不受Token价格束缚、完全私密。

中国AI大模型周调用量增31.48%,连续五周超美国

2026年4月6日 11:29
根据OpenRouter最新数据测算,上周(3月30日至4月5日)全球AI大模型总调用量为27万亿Token,环比增长18.9%。其中,上榜的AI大模型中,中国AI大模型的周调用量上升至12.96万亿Token,较此前一周上涨31.48%;美国AI大模型周调用量为3.03万亿Token,环比增长0.76%。中国AI大模型周调用量连续五周增长,且连续五周超越美国。(每日经济新闻)

多家券商最新策略观点:后市不悲观,静待进攻号

2026年4月6日 11:14
从多家证券研究所的最新策略观点来看,尽管目前投资者观望情绪浓厚,但资金面上并未出现悲观信号,居民资金入市大势不改。预计在地缘冲突的靴子落地之前,市场仍将延续震荡趋势,耐心等待反攻号角的吹响,或许才是当下更稳妥的对策。(证券时报)

华泰证券:看好中国头部家电企业继续提升中东市场份额

2026年4月6日 10:43
华泰证券研报表示,中东地缘冲突持续,霍尔木兹海峡航线受阻、区域消费信心短期承压,对我国家电出口带来一定扰动。若海峡持续封锁,此类高强度扰动带来的短期冲击主要体现在海湾国家终端消费信心走弱、经销商库存趋于保守、进入海湾港口航次减少、保险和运价上升,从而压制家电订单节奏;但从中长期看,海湾国家人口增长、地产与基建扩张、高温气候强化空调刚需,以及中国品牌持续替代日韩品牌的逻辑并未改变。华泰证券看好中国头部家电企业继续提升中东市场份额。(证券时报)

A股将有73.18亿股限售股解禁,总市值约1090.02亿元

2026年4月6日 10:29
受清明节假期影响,今天A股休市,明天起照常开市;而港股受多个假期叠加影响,今明两天港股都休市,周三恢复正常交易。此外,今天到4月10日,A股市场将有30家公司的限售股迎来解禁,解禁股份总数达73.18亿股。以4月3日的收盘价为基准计算,这批解禁股票的总市值约为1090.02亿元。(央视新闻)

六部门:支持符合条件的电商企业发行债券融资 优化融资等政策流程

2026年4月6日 10:20
商务部等6部门发布关于更好服务实体经济 推进电子商务高质量发展的指导意见。其中提到,发挥产业投资基金和科创母基金作用,在依法合规、风险可控前提下,鼓励金融机构结合电商企业融资需求,完善多样化的金融服务模式。综合运用贷款、股权等手段,为电商业态模式创新提供全链条全生命周期、多元化接力式金融服务。鼓励金融机构与电商企业合作,创新信贷产品和服务。支持符合条件的电商企业发行债券融资,优化融资等政策流程,支持符合条件的电商企业在境内外上市融资。(财联社)

vite 是如何加载解析 vite.config.js 配置文件的?

作者 米丘
2026年4月6日 10:11

当我们在终端运行 vite dev,Vite 启动开发服务器的首个关键步骤就是解析配置。本文将深入剖析 Vite 加载配置文件的三种模式。

loadConfigFromFile 的完整流程

loadConfigFromFile 是配置文件加载的核心函数,其完整流程如下:

  1. 确定配置文件路径(自动查找或使用 --config 指定的路径)。
  2. 根据文件后缀和 package.json 中的 type 字段判断模块格式(是否为 ESM)。
  3. 根据 configLoader加载器配置来加载配置文件和转换代码。
    • bundle模式,调用 bundleConfigFile 使用 rolldown 打包配置文件,获取转换后的代码和依赖列表。调用 loadConfigFromBundledFile 将打包后的代码转成配置对象。
    • runner模式,使用 Vite 的 ModuleRunner 动态转换任何文件。它的核心机制是利用 RunnableDevEnvironment 提供的 runner.import 函数,在独立的执行环境中加载并运行模块
    • native模式,利用原生动态引入。
  4. 如果用户导出的是函数,则调用该函数传入 configEnv(包含 commandmode 等参数),获取最终配置对象。
  5. 返回配置对象、配置文件路径以及依赖列表 dependencies
  let { configFile } = config
  if (configFile !== false) {
    // 从文件加载配置
    const loadResult = await loadConfigFromFile(
      configEnv,
      configFile,
      config.root,
      config.logLevel,
      config.customLogger,
      config.configLoader,
    )
    if (loadResult) {
      config = mergeConfig(loadResult.config, config)
      configFile = loadResult.path
      configFileDependencies = loadResult.dependencies
    }
  }

image.png

如果在执行 vite dev 时没有使用 --config 参数指定配置文件,Vite 将按照以下顺序自动查找并加载配置文件。

const DEFAULT_CONFIG_FILES: string[] = [
  'vite.config.js',
  'vite.config.mjs',
  'vite.config.ts',
  'vite.config.cjs',
  'vite.config.mts',
  'vite.config.cts',
]

Vite 提供了三种配置加载机制

当配置文件被定位后,Vite 如何读取并执行它的内容?这取决于 configLoader 配置选项。Vite 提供了三种机制来加载配置文件,默认使用 bundle 模式。

const resolver =
  configLoader === 'bundle'
    ? bundleAndLoadConfigFile // 处理配置文件的预构建
    : configLoader === 'runner'
      ? runnerImportConfigFile // 处理配置文件的运行时导入
      : nativeImportConfigFile // 处理配置文件的原生导入

bundle (默认)

使用打包工具(Rolldown)将配置文件及其依赖打包成一个临时文件,再加载执行。

function bundleAndLoadConfigFile(resolvedPath: string) {
  // 检查是否为 ESM 模块
  const isESM =
    // 在 Deno 环境中运行
    typeof process.versions.deno === 'string' || isFilePathESM(resolvedPath)

  // 配置文件打包
  // 打包过程会处理配置文件的依赖,将其转换为可执行的代码
  const bundled = await bundleConfigFile(resolvedPath, isESM)
  // 配置加载
  const userConfig = await loadConfigFromBundledFile(
    resolvedPath,
    bundled.code,
    isESM,
  )

  return {
    // 加载的用户配置
    configExport: userConfig,
    // 配置文件的依赖项
    dependencies: bundled.dependencies,
  }
}

image.png

image.png

image.png

image.png

image.png

image.png

bundle.code 字符串

import "node:module";
import { defineConfig } from "file:///Users/xxxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/vite/dist/node/index.js";
import vue from "file:///Users/xxxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/@vitejs/plugin-vue/dist/index.mjs";
import vueJsx from "file:///Users/xxxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/@vitejs/plugin-vue-jsx/dist/index.mjs";
import VueRouter from "file:///Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/vue-router/dist/unplugin/vite.mjs";
Object.create;
Object.defineProperty;
Object.getOwnPropertyDescriptor;
Object.getOwnPropertyNames;
Object.getPrototypeOf;
Object.prototype.hasOwnProperty;
var vite_config_default = defineConfig({
plugins: [
VueRouter({
routesFolder: "src/pages",
extensions: [".vue"],
dts: "src/typed-router.d.ts",
importMode: "async",
root: process.cwd(),
watch: true
}),
vue(),
vueJsx()
],
resolve: { alias: { "@": "/src" } },
css: { preprocessorOptions: { less: {
additionalData: \`@import "@/styles/variables.less";\`,
javascriptEnabled: true
} } },
mode: "development"
});
//#endregion
export { vite_config_default as default };

//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidml0ZS5jb25maWcuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiL1VzZXJzL2hhaXlhbi9Eb2N1bWVudHMvY29kZS9jbG91ZGNvZGUvdnVlMy12aXRlLWN1YmUvdml0ZS5jb25maWcudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLy8gaW1wb3J0IHsgZmlsZVVSTFRvUGF0aCwgVVJMIH0gZnJvbSAnbm9kZTp1cmwnXG5pbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHZ1ZSBmcm9tICdAdml0ZWpzL3BsdWdpbi12dWUnXG5pbXBvcnQgdnVlSnN4IGZyb20gJ0B2aXRlanMvcGx1Z2luLXZ1ZS1qc3gnXG5pbXBvcnQgdnVlRGV2VG9vbHMgZnJvbSAndml0ZS1wbHVnaW4tdnVlLWRldnRvb2xzJ1xuaW1wb3J0IFZ1ZVJvdXRlciBmcm9tICd2dWUtcm91dGVyL3ZpdGUnXG5cblxuLy8gaHR0cHM6Ly92aXRlLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBwbHVnaW5zOiBbXG4gICAgLy8g5b+F6aG76KaB5ZyoIHZ1ZSDmj5Lku7bkuYvliY1cbiAgICBWdWVSb3V0ZXIoe1xuICAgICAgcm91dGVzRm9sZGVyOiAnc3JjL3BhZ2VzJywgLy8g6buY6K6kIHBhZ2VzXG4gICAgICBleHRlbnNpb25zOiBbJy52dWUnXSwgLy8g5Yy56YWN5paH5Lu25ZCO57yAXG4gICAgICBkdHM6ICdzcmMvdHlwZWQtcm91dGVyLmQudHMnLCAvLyDnlJ/miJDnsbvlnovmlofku7ZcbiAgICAgIC8vIOWHuueOsCBSYW5nZUVycm9yOiBNYXhpbXVtIGNhbGwgc3RhY2sgc2l6ZSBleGNlZWRlZFxuICAgICAgLy8gZ2V0Um91dGVOYW1lOiAocm91dGUpID0+IHtcbiAgICAgIC8vICAgY29uc29sZS5sb2coJ2dldFJvdXRlTmFtZScscm91dGUpXG4gICAgICAvLyAgIHJldHVybiByb3V0ZS5uYW1lIHx8IHJvdXRlLnBhdGhcbiAgICAgIC8vIH0sXG5cbiAgICAgICAvLyDmt7vliqDosIPor5XpgInpoblcbiAgICAgIC8vIGxvZ3M6IHRydWUsXG5cbiAgICAgIC8vIHJvdXRlQmxvY2tMYW5nOiAnanNvbjUnLCAvLyDot6/nlLHlnZfor63oqIDvvIzpu5jorqQganNvblxuICAgICAgaW1wb3J0TW9kZTogJ2FzeW5jJyxcbiAgICAgIHJvb3Q6IHByb2Nlc3MuY3dkKCksXG5cbiAgICAgIC8vIOWcqOmFjee9ruaWh+S7tuWGmeWFpeWJje+8jOaJi+WKqOS/ruaUuei3r+eUsemFjee9ru+8iOWmgua3u+WKoOWFqOWxgOi3r+eUseWuiOWNq+OAgeiwg+aVtOi3r+eUseWFg+S/oeaBr+OAgei/h+a7pOi3r+eUseetie+8iVxuICAgICAgLy8gYmVmb3JlV3JpdGVGaWxlczogKGVkaXRlZFJvdXRlcykgPT4ge1xuICAgICAgLy8gICBjb25zb2xlLmxvZygnYmVmb3JlV3JpdGVGaWxlcycsIGVkaXRlZFJvdXRlcylcbiAgICAgIC8vIH0sXG4gICAgICB3YXRjaDogdHJ1ZSwgLy8g5byA5ZCv6Lev55Sx5Z2X5paH5Lu255uR5ZCsXG4gICAgICAvLyDlvIDlkK/lrp7pqozmgKflip/og71cbiAgICAgIC8vIGV4cGVyaW1lbnRhbDoge1xuICAgICAgIFxuICAgICAgLy8gfSxcbiAgICB9KSxcbiAgICB2dWUoKSxcbiAgICB2dWVKc3goKSxcbiAgICAvLyB2dWVEZXZUb29scygpLFxuICBdLFxuICByZXNvbHZlOiB7XG4gICAgLy8gYWxpYXM6IHtcbiAgICAvLyAgICdAJzogZmlsZVVSTFRvUGF0aChuZXcgVVJMKCcuL3NyYycsIGltcG9ydC5tZXRhLnVybCkpXG4gICAgLy8gfSxcbiAgICBhbGlhczoge1xuICAgICAgJ0AnOiAnL3NyYycsXG4gICAgfSxcbiAgICAvLyB0c2NvbmZpZ1BhdGhzOiB0cnVlLCAgLy8g6Ieq5Yqo6K+75Y+WIHRzY29uZmlnIHBhdGhzXG4gIH0sXG4gIGNzczoge1xuICAgIHByZXByb2Nlc3Nvck9wdGlvbnM6IHtcbiAgICAgIGxlc3M6IHtcbiAgICAgICAgYWRkaXRpb25hbERhdGE6IGBAaW1wb3J0IFwiQC9zdHlsZXMvdmFyaWFibGVzLmxlc3NcIjtgLFxuICAgICAgICBqYXZhc2NyaXB0RW5hYmxlZDogdHJ1ZVxuICAgICAgfVxuICAgIH1cbiAgfSxcbiAgbW9kZTogJ2RldmVsb3BtZW50JyxcbiAgLy8gc2VydmVyOiB7XG4gIC8vICAgd3M6IGZhbHNlLFxuICAvLyB9LFxuICAvLyBvcHRpbWl6ZURlcHM6IHtcbiAgLy8gICBpbmNsdWRlOiBbJ3ZpcnR1YWw6dnVlLWluc3BlY3Rvci1wYXRoOmxvYWQuanMnXSxcbiAgLy8gfSxcblxufSkiXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7O0FBU0EsSUFBQSxzQkFBZSxhQUFhO0NBQzFCLFNBQVM7RUFFUCxVQUFVO0dBQ1IsY0FBYztHQUNkLFlBQVksQ0FBQyxPQUFPO0dBQ3BCLEtBQUs7R0FXTCxZQUFZO0dBQ1osTUFBTSxRQUFRLEtBQUs7R0FNbkIsT0FBTztHQUtSLENBQUM7RUFDRixLQUFLO0VBQ0wsUUFBUTtFQUVUO0NBQ0QsU0FBUyxFQUlQLE9BQU8sRUFDTCxLQUFLLFFBQ04sRUFFRjtDQUNELEtBQUssRUFDSCxxQkFBcUIsRUFDbkIsTUFBTTtFQUNKLGdCQUFnQjtFQUNoQixtQkFBbUI7RUFDckIsRUFDRixFQUNEO0NBQ0QsTUFBTTtDQVFQLENBQUEifQ==

dependencies

[
  "/Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/vite.config.ts",
]

临时文件

image.png

image.png

vue3-vite-cube/node_modules/.vite-temp/vite.config.ts.timestamp-1775361732369-f30607f0da0d6.mjs 文件内容如下:

import "node:module";
import { defineConfig } from "file:///Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/vite/dist/node/index.js";
import vue from "file:///Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/@vitejs/plugin-vue/dist/index.mjs";
import vueJsx from "file:///Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/@vitejs/plugin-vue-jsx/dist/index.mjs";
import VueRouter from "file:///Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/vue-router/dist/unplugin/vite.mjs";
Object.create;
Object.defineProperty;
Object.getOwnPropertyDescriptor;
Object.getOwnPropertyNames;
Object.getPrototypeOf;
Object.prototype.hasOwnProperty;
var vite_config_default = defineConfig({
plugins: [
VueRouter({
routesFolder: "src/pages",
extensions: [".vue"],
dts: "src/typed-router.d.ts",
importMode: "async",
root: process.cwd(),
watch: true
}),
vue(),
vueJsx()
],
resolve: { alias: { "@": "/src" } },
css: { preprocessorOptions: { less: {
additionalData: `@import "@/styles/variables.less";`,
javascriptEnabled: true
} } },
mode: "development"
});
//#endregion
export { vite_config_default as default };

//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidml0ZS5jb25maWcuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiL1VzZXJzL2hhaXlhbi9Eb2N1bWVudHMvY29kZS9jbG91ZGNvZGUvdnVlMy12aXRlLWN1YmUvdml0ZS5jb25maWcudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLy8gaW1wb3J0IHsgZmlsZVVSTFRvUGF0aCwgVVJMIH0gZnJvbSAnbm9kZTp1cmwnXG5pbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHZ1ZSBmcm9tICdAdml0ZWpzL3BsdWdpbi12dWUnXG5pbXBvcnQgdnVlSnN4IGZyb20gJ0B2aXRlanMvcGx1Z2luLXZ1ZS1qc3gnXG5pbXBvcnQgdnVlRGV2VG9vbHMgZnJvbSAndml0ZS1wbHVnaW4tdnVlLWRldnRvb2xzJ1xuaW1wb3J0IFZ1ZVJvdXRlciBmcm9tICd2dWUtcm91dGVyL3ZpdGUnXG5cblxuLy8gaHR0cHM6Ly92aXRlLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBwbHVnaW5zOiBbXG4gICAgLy8g5b+F6aG76KaB5ZyoIHZ1ZSDmj5Lku7bkuYvliY1cbiAgICBWdWVSb3V0ZXIoe1xuICAgICAgcm91dGVzRm9sZGVyOiAnc3JjL3BhZ2VzJywgLy8g6buY6K6kIHBhZ2VzXG4gICAgICBleHRlbnNpb25zOiBbJy52dWUnXSwgLy8g5Yy56YWN5paH5Lu25ZCO57yAXG4gICAgICBkdHM6ICdzcmMvdHlwZWQtcm91dGVyLmQudHMnLCAvLyDnlJ/miJDnsbvlnovmlofku7ZcbiAgICAgIC8vIOWHuueOsCBSYW5nZUVycm9yOiBNYXhpbXVtIGNhbGwgc3RhY2sgc2l6ZSBleGNlZWRlZFxuICAgICAgLy8gZ2V0Um91dGVOYW1lOiAocm91dGUpID0+IHtcbiAgICAgIC8vICAgY29uc29sZS5sb2coJ2dldFJvdXRlTmFtZScscm91dGUpXG4gICAgICAvLyAgIHJldHVybiByb3V0ZS5uYW1lIHx8IHJvdXRlLnBhdGhcbiAgICAgIC8vIH0sXG5cbiAgICAgICAvLyDmt7vliqDosIPor5XpgInpoblcbiAgICAgIC8vIGxvZ3M6IHRydWUsXG5cbiAgICAgIC8vIHJvdXRlQmxvY2tMYW5nOiAnanNvbjUnLCAvLyDot6/nlLHlnZfor63oqIDvvIzpu5jorqQganNvblxuICAgICAgaW1wb3J0TW9kZTogJ2FzeW5jJyxcbiAgICAgIHJvb3Q6IHByb2Nlc3MuY3dkKCksXG5cbiAgICAgIC8vIOWcqOmFjee9ruaWh+S7tuWGmeWFpeWJje+8jOaJi+WKqOS/ruaUuei3r+eUsemFjee9ru+8iOWmgua3u+WKoOWFqOWxgOi3r+eUseWuiOWNq+OAgeiwg+aVtOi3r+eUseWFg+S/oeaBr+OAgei/h+a7pOi3r+eUseetie+8iVxuICAgICAgLy8gYmVmb3JlV3JpdGVGaWxlczogKGVkaXRlZFJvdXRlcykgPT4ge1xuICAgICAgLy8gICBjb25zb2xlLmxvZygnYmVmb3JlV3JpdGVGaWxlcycsIGVkaXRlZFJvdXRlcylcbiAgICAgIC8vIH0sXG4gICAgICB3YXRjaDogdHJ1ZSwgLy8g5byA5ZCv6Lev55Sx5Z2X5paH5Lu255uR5ZCsXG4gICAgICAvLyDlvIDlkK/lrp7pqozmgKflip/og71cbiAgICAgIC8vIGV4cGVyaW1lbnRhbDoge1xuICAgICAgIFxuICAgICAgLy8gfSxcbiAgICB9KSxcbiAgICB2dWUoKSxcbiAgICB2dWVKc3goKSxcbiAgICAvLyB2dWVEZXZUb29scygpLFxuICBdLFxuICByZXNvbHZlOiB7XG4gICAgLy8gYWxpYXM6IHtcbiAgICAvLyAgICdAJzogZmlsZVVSTFRvUGF0aChuZXcgVVJMKCcuL3NyYycsIGltcG9ydC5tZXRhLnVybCkpXG4gICAgLy8gfSxcbiAgICBhbGlhczoge1xuICAgICAgJ0AnOiAnL3NyYycsXG4gICAgfSxcbiAgICAvLyB0c2NvbmZpZ1BhdGhzOiB0cnVlLCAgLy8g6Ieq5Yqo6K+75Y+WIHRzY29uZmlnIHBhdGhzXG4gIH0sXG4gIGNzczoge1xuICAgIHByZXByb2Nlc3Nvck9wdGlvbnM6IHtcbiAgICAgIGxlc3M6IHtcbiAgICAgICAgYWRkaXRpb25hbERhdGE6IGBAaW1wb3J0IFwiQC9zdHlsZXMvdmFyaWFibGVzLmxlc3NcIjtgLFxuICAgICAgICBqYXZhc2NyaXB0RW5hYmxlZDogdHJ1ZVxuICAgICAgfVxuICAgIH1cbiAgfSxcbiAgbW9kZTogJ2RldmVsb3BtZW50JyxcbiAgLy8gc2VydmVyOiB7XG4gIC8vICAgd3M6IGZhbHNlLFxuICAvLyB9LFxuICAvLyBvcHRpbWl6ZURlcHM6IHtcbiAgLy8gICBpbmNsdWRlOiBbJ3ZpcnR1YWw6dnVlLWluc3BlY3Rvci1wYXRoOmxvYWQuanMnXSxcbiAgLy8gfSxcblxufSkiXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7O0FBU0EsSUFBQSxzQkFBZSxhQUFhO0NBQzFCLFNBQVM7RUFFUCxVQUFVO0dBQ1IsY0FBYztHQUNkLFlBQVksQ0FBQyxPQUFPO0dBQ3BCLEtBQUs7R0FXTCxZQUFZO0dBQ1osTUFBTSxRQUFRLEtBQUs7R0FNbkIsT0FBTztHQUtSLENBQUM7RUFDRixLQUFLO0VBQ0wsUUFBUTtFQUVUO0NBQ0QsU0FBUyxFQUlQLE9BQU8sRUFDTCxLQUFLLFFBQ04sRUFFRjtDQUNELEtBQUssRUFDSCxxQkFBcUIsRUFDbkIsTUFBTTtFQUNKLGdCQUFnQjtFQUNoQixtQkFBbUI7RUFDckIsRUFDRixFQUNEO0NBQ0QsTUFBTTtDQVFQLENBQUEifQ==
/**
 * 用于从打包后的代码加载 Vite 配置。
 * 它根据模块类型(ESM 或 CommonJS)采用不同的加载策略,确保配置文件能够被正确执行并返回配置对象
 * @param fileName  文件路径
 * @param bundledCode 打包转换后代码
 * @param isESM 是否为 ESM 格式
 * @returns 
 */
async function loadConfigFromBundledFile(
  fileName: string,
  bundledCode: string,
  isESM: boolean,
): Promise<UserConfigExport> {
  // for esm, before we can register loaders without requiring users to run node
  // with --experimental-loader themselves, we have to do a hack here:
  // write it to disk, load it with native Node ESM, then delete the file.
  if (isESM) {
    // Storing the bundled file in node_modules/ is avoided for Deno
    // because Deno only supports Node.js style modules under node_modules/
    // and configs with `npm:` import statements will fail when executed.
    // 查找最近的 node_modules 目录
    let nodeModulesDir =
      typeof process.versions.deno === 'string'
        ? undefined
        : findNearestNodeModules(path.dirname(fileName))

    if (nodeModulesDir) {
      try {
        // 创建临时目录
        // node_modules/.vite-temp/
        await fsp.mkdir(path.resolve(nodeModulesDir, '.vite-temp/'), {
          recursive: true,
        })
      } catch (e) {
        if (e.code === 'EACCES') {
          // If there is no access permission, a temporary configuration file is created by default.
          nodeModulesDir = undefined
        } else {
          throw e
        }
      }
    }
    // 生成 hash 值
    const hash = `timestamp-${Date.now()}-${Math.random().toString(16).slice(2)}`
    // 生成临时文件名
    const tempFileName = nodeModulesDir
      ? path.resolve(
          nodeModulesDir,
          `.vite-temp/${path.basename(fileName)}.${hash}.mjs`,
        )
      : `${fileName}.${hash}.mjs`
      // 写入临时文件
    await fsp.writeFile(tempFileName, bundledCode)
    try {
      // 将文件系统路径转换为 file:// 协议的 URL 对象
      // 原因:ESM 的 import() 语法要求模块标识符为 URL 格式(对于本地文件),不能直接使用文件系统路径
      // 动态加载 ESM 格式配置文件
      // 执行过程:
      // 1、Node.js 读取并执行 tempFileName 指向的文件
      // 2、执行文件中的代码,构建模块的导出
      // 3、生成包含所有导出的模块命名空间对象
      // 4、Promise 解析为该命名空间对象
      return (await import(pathToFileURL(tempFileName).href)).default
    } finally {
      fs.unlink(tempFileName, () => {}) // Ignore errors
    }
  }
  // for cjs, we can register a custom loader via `_require.extensions`
  else {
    // 获取文件扩展名
    const extension = path.extname(fileName)
    // We don't use fsp.realpath() here because it has the same behaviour as
    // fs.realpath.native. On some Windows systems, it returns uppercase volume
    // letters (e.g. "C:\") while the Node.js loader uses lowercase volume letters.
    // See https://github.com/vitejs/vite/issues/12923
    // 获取文件的真实路径
    // 避免 Windows 系统上的路径大小写问题
    const realFileName = await promisifiedRealpath(fileName)
    // 确定加载器扩展名
    // require.extensions 标记已废弃
    const loaderExt = extension in _require.extensions ? extension : '.js'
    const defaultLoader = _require.extensions[loaderExt]!
    // 注册自定义加载器
    _require.extensions[loaderExt] = (module: NodeModule, filename: string) => {
      if (filename === realFileName) {
        // 执行打包后的代码
        ;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
      } else {
        // 使用默认加载器
        defaultLoader(module, filename)
      }
    }
    // clear cache in case of server restart
    // 清除缓存
    delete _require.cache[_require.resolve(fileName)]
    // 加载配置文件
    const raw = _require(fileName)
    // 恢复默认加载器
    _require.extensions[loaderExt] = defaultLoader
    return raw.__esModule ? raw.default : raw
  }
}

runner (实验性)

runner 模式不会创建临时配置文件,而是使用 Vite 的 ModuleRunner 动态转换任何文件。它的核心机制是利用 RunnableDevEnvironment 提供的 runner.import 函数,在独立的执行环境中加载并运行模块。

{
   "start": "vite --configLoader=runner",
}
/**
 * 用于通过 runner 方式导入配置文件。
 * 它使用 runnerImport 函数动态加载配置文件,提取默认导出作为配置对象,并返回配置对象及其依赖项。
 * @param resolvedPath 配置文件路径
 * @returns 
 */
async function runnerImportConfigFile(resolvedPath: string) {
  const { module, dependencies } = await runnerImport<{
    default: UserConfigExport
  }>(resolvedPath)
  return {
    configExport: module.default,
    dependencies,
  }
}

image.png

async function runnerImport<T>(
  moduleId: string,
  inlineConfig?: InlineConfig,
): Promise<RunnerImportResult<T>> {

  // 模块同步条件检查
  const isModuleSyncConditionEnabled = (await import('#module-sync-enabled'))
    .default

  // 配置解析
  const config = await resolveConfig(
    // 合并配置
    mergeConfig(inlineConfig || {}, {
      configFile: false, // 禁用配置文件解析
      envDir: false, // 禁用环境变量目录解析
      cacheDir: process.cwd(), // 缓存目录设置为当前工作目录
      environments: {
        inline: {
          // 指定环境的消费方为服务器端
          consumer: 'server',
          dev: {
            // 启用模块运行器转换
            moduleRunnerTransform: true,
          },
          // 模块解析配置
          resolve: {
            // 启用外部模块解析,将依赖视为外部模块,不进行打包
            // 影响:减少打包体积,提高模块加载速度
            external: true,
            // 清空主字段数组
            // 不使用 package.json 中的主字段进行模块解析
            // 避免因主字段优先级导致的解析问题,确保一致性
            mainFields: [],
            // 指定模块解析条件
            conditions: [
              'node',
              ...(isModuleSyncConditionEnabled ? ['module-sync'] : []),
            ],
          },
        },
      },
    } satisfies InlineConfig),
    'serve', // 确保是 serve 命令
  )
  // 创建可运行的开发环境
  const environment = createRunnableDevEnvironment('inline', config, {
    runnerOptions: {
      hmr: {
        logger: false, // 禁用 HMR 日志记录
      },
    },
    hot: false, // 禁用 HMR
  })
  // 初始化环境
  // 准备模块运行器,确保能够正确加载模块
  await environment.init()
  try {
    // 使用环境的运行器导入模块
    // 模块加载与执行:
    // 1、ModuleRunner 解析 moduleId,处理路径解析
    // 2、加载模块文件内容
    // 3、应用必要的转换(如 ESM 到 CommonJS 的转换)
    // 4、执行模块代码
    // 5、收集模块的依赖项
    const module = await environment.runner.import(moduleId)

    // 获取所有评估过的模块
    const modules = [
      ...environment.runner.evaluatedModules.urlToIdModuleMap.values(),
    ]
    // 过滤出所有外部化模块和当前模块
    // 这些模块不是依赖项,因为它们是 Vite 内部使用的模块
    const dependencies = modules
      .filter((m) => {
        // ignore all externalized modules
        // 忽略没有meta的模块 或者标记为外部化的模块
        if (!m.meta || 'externalize' in m.meta) {
          return false
        }
        // ignore the current module
        // 忽略当前模块,因为它不是依赖项
        return m.exports !== module
      })
      .map((m) => m.file)

    return {
      module,
      dependencies,
    }
  } finally {
    // 关闭环境
    // 释放所有资源,避免内存泄漏等问题
    await environment.close()
  }
}

image.png

module

{
  default: {
    plugins: [
      {
        name: "vue-router",
        enforce: "pre",
        resolveId: {
          filter: {
            id: {
              include: [
                {
                },
                {
                },
                {
                },
              ],
            },
          },
          handler: function(...args) {
            const [id] = args;
            if (!supportNativeFilter(this, key) && !filter(id)) return;
            return handler.apply(this, args);
          },
        },
        buildStart: async buildStart() {
          await ctx.scanPages(options.watch);
        },
        buildEnd: function() {
          ctx.stopWatcher();
        },
        transform: {
          filter: {
            id: {
              include: [
                "/Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/src/pages/**/*.vue",
                {
                },
              ],
              exclude: [
              ],
            },
          },
          handler: function(...args) {
            const [code, id] = args;
            if (plugin.transformInclude && !plugin.transformInclude(id)) return;
            if (!supportNativeFilter(this, key) && !filter(id, code)) return;
            return handler.apply(this, args);
          },
        },
        load: {
          filter: {
            id: {
              include: [
                {
                },
                {
                },
                {
                },
              ],
            },
          },
          handler: function(...args) {
            const [id] = args;
            if (plugin.loadInclude && !plugin.loadInclude(id)) return;
            if (!supportNativeFilter(this, key) && !filter(id)) return;
            return handler.apply(this, args);
          },
        },
        vite: {
          configureServer: function(server) {
            ctx.setServerContext(createViteContext(server));
          },
        },
        configureServer: function(server) {
          ctx.setServerContext(createViteContext(server));
        },
      },
      {
        name: "vite:vue",
        api: {
          options: {
            isProduction: false,
            compiler: null,
            customElement: {
            },
            root: "/Users/xxxx/Documents/code/cloudcode/vue3-vite-cube",
            sourceMap: true,
            cssDevSourcemap: false,
          },
          include: {
          },
          exclude: undefined,
          version: "6.0.5",
        },
        handleHotUpdate: function(ctx) {
          ctx.server.ws.send({
          type: "custom",
          event: "file-changed",
          data: { file: normalizePath(ctx.file) }
          });
          if (options.value.compiler.invalidateTypeCache) options.value.compiler.invalidateTypeCache(ctx.file);
          let typeDepModules;
          const matchesFilter = filter.value(ctx.file);
          if (typeDepToSFCMap.has(ctx.file)) {
          typeDepModules = handleTypeDepChange(typeDepToSFCMap.get(ctx.file), ctx);
          if (!matchesFilter) return typeDepModules;
          }
          if (matchesFilter) return handleHotUpdate(ctx, options.value, customElementFilter.value(ctx.file), typeDepModules);
        },
        config: function(config) {
          const parseDefine = (v) => {
          try {
          return typeof v === "string" ? JSON.parse(v) : v;
          } catch (err) {
          return v;
          }
          };
          return {
          resolve: { dedupe: config.build?.ssr ? [] : ["vue"] },
          define: {
          __VUE_OPTIONS_API__: options.value.features?.optionsAPI ?? parseDefine(config.define?.__VUE_OPTIONS_API__) ?? true,
          __VUE_PROD_DEVTOOLS__: (options.value.features?.prodDevtools || parseDefine(config.define?.__VUE_PROD_DEVTOOLS__)) ?? false,
          __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: (options.value.features?.prodHydrationMismatchDetails || parseDefine(config.define?.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__)) ?? false
          },
          ssr: { external: config.legacy?.buildSsrCjsExternalHeuristics ? ["vue", "@vue/server-renderer"] : [] }
          };
        },
        configResolved: function(config) {
          options.value = {
          ...options.value,
          root: config.root,
          sourceMap: config.command === "build" ? !!config.build.sourcemap : true,
          cssDevSourcemap: config.css?.devSourcemap ?? false,
          isProduction: config.isProduction,
          devToolsEnabled: !!(options.value.features?.prodDevtools || config.define.__VUE_PROD_DEVTOOLS__ || !config.isProduction)
          };
          const _warn = config.logger.warn;
          config.logger.warn = (...args) => {
          if (args[0].match(/\[lightningcss\] '(deep|slotted|global)' is not recognized as a valid pseudo-/)) return;
          _warn(...args);
          };
          transformCachedModule = config.command === "build" && options.value.sourceMap && config.build.watch != null;
        },
        options: function() {
          optionsHookIsCalled = true;
          plugin.transform.filter = { id: {
          include: [...makeIdFiltersToMatchWithQuery(ensureArray(include.value)), /[?&]vue\b/],
          exclude: exclude.value
          } };
        },
        shouldTransformCachedModule: function({ id }) {
          if (transformCachedModule && parseVueRequest(id).query.vue) return true;
          return false;
        },
        configureServer: function(server) {
          options.value.devServer = server;
        },
        buildStart: function() {
          const compiler = options.value.compiler = options.value.compiler || resolveCompiler(options.value.root);
          if (compiler.invalidateTypeCache) options.value.devServer?.watcher.on("unlink", (file) => {
          compiler.invalidateTypeCache(file);
          });
        },
        resolveId: {
          filter: {
            id: [
              {
              },
              {
              },
            ],
          },
          handler: function(id) {
            if (id === EXPORT_HELPER_ID) return id;
            if (parseVueRequest(id).query.vue) return id;
          },
        },
        load: {
          filter: {
            id: [
              {
              },
              {
              },
            ],
          },
          handler: function(id, opt) {
            if (id === EXPORT_HELPER_ID) return helperCode;
            const ssr = opt?.ssr === true;
            const { filename, query } = parseVueRequest(id);
            if (query.vue) {
            if (query.src) return fs.readFileSync(filename, "utf-8");
            const descriptor = getDescriptor(filename, options.value);
            let block;
            if (query.type === "script") block = resolveScript(descriptor, options.value, ssr, customElementFilter.value(filename));
            else if (query.type === "template") block = descriptor.template;
            else if (query.type === "style") block = descriptor.styles[query.index];
            else if (query.index != null) block = descriptor.customBlocks[query.index];
            if (block) return {
            code: block.content,
            map: block.map
            };
            }
          },
        },
        transform: {
          handler: function(code, id, opt) {
            const ssr = opt?.ssr === true;
            const { filename, query } = parseVueRequest(id);
            if (query.raw || query.url) return;
            if (!filter.value(filename) && !query.vue) return;
            if (!query.vue) return transformMain(code, filename, options.value, this, ssr, customElementFilter.value(filename));
            else {
            const descriptor = query.src ? getSrcDescriptor(filename, query) || getTempSrcDescriptor(filename, query) : getDescriptor(filename, options.value);
            if (query.src) this.addWatchFile(filename);
            if (query.type === "template") return transformTemplateAsModule(code, filename, descriptor, options.value, this, ssr, customElementFilter.value(filename));
            else if (query.type === "style") return transformStyle(code, descriptor, Number(query.index || 0), options.value, this, filename);
            }
          },
        },
      },
      {
        name: "vite:vue-jsx",
        config: function(config) {
          const parseDefine = (v) => {
          try {
          return typeof v === "string" ? JSON.parse(v) : v;
          } catch (err) {
          return v;
          }
          };
          const isRolldownVite = this && "rolldownVersion" in this.meta;
          return {
          [isRolldownVite ? "oxc" : "esbuild"]: tsTransform === "built-in" ? { exclude: /\.jsx?$/ } : { include: /\.ts$/ },
          define: {
          __VUE_OPTIONS_API__: parseDefine(config.define?.__VUE_OPTIONS_API__) ?? true,
          __VUE_PROD_DEVTOOLS__: parseDefine(config.define?.__VUE_PROD_DEVTOOLS__) ?? false,
          __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: parseDefine(config.define?.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__) ?? false
          },
          optimizeDeps: isRolldownVite ? { rolldownOptions: { transform: { jsx: "preserve" } } } : {}
          };
        },
        configResolved: function(config) {
          needHmr = config.command === "serve" && !config.isProduction;
          needSourceMap = config.command === "serve" || !!config.build.sourcemap;
          root = config.root;
        },
        resolveId: {
          filter: {
            id: {
            },
          },
          handler: function(id) {
            if (id === ssrRegisterHelperId) return id;
          },
        },
        load: {
          filter: {
            id: {
            },
          },
          handler: function(id) {
            if (id === ssrRegisterHelperId) return ssrRegisterHelperCode;
          },
        },
        transform: {
          order: undefined,
          filter: {
            id: {
              include: {
              },
              exclude: undefined,
            },
          },
          handler: async handler(code, id, opt) {
            const ssr = opt?.ssr === true;
            const [filepath] = id.split("?");
            if (filter(id) || filter(filepath)) {
            const plugins = [[jsx, babelPluginOptions], ...babelPlugins];
            if (id.endsWith(".tsx") || filepath.endsWith(".tsx")) if (tsTransform === "built-in") plugins.push([await import("@babel/plugin-syntax-typescript").then((r) => r.default), { isTSX: true }]);
            else plugins.push([await import("@babel/plugin-transform-typescript").then((r) => r.default), {
            ...tsPluginOptions,
            isTSX: true,
            allowExtensions: true
            }]);
            if (!ssr && !needHmr) plugins.push(() => {
            return { visitor: { CallExpression: { enter(_path) {
            if (isDefineComponentCall(_path.node, defineComponentName)) {
            const callee = _path.node.callee;
            callee.name = `/* @__PURE__ */ ${callee.name}`;
            }
            } } } };
            });
            else plugins.push(() => {
            return { visitor: { ExportDefaultDeclaration: { enter(_path) {
            const unwrappedDeclaration = unwrapTypeAssertion(_path.node.declaration);
            if (isDefineComponentCall(unwrappedDeclaration, defineComponentName)) {
            const declaration = unwrappedDeclaration;
            const nodesPath = _path.replaceWithMultiple([types.variableDeclaration("const", [types.variableDeclarator(types.identifier("__default__"), types.callExpression(declaration.callee, declaration.arguments))]), types.exportDefaultDeclaration(types.identifier("__default__"))]);
            _path.scope.registerDeclaration(nodesPath[0]);
            }
            } } } };
            });
            const result = babel.transformSync(code, {
            babelrc: false,
            ast: true,
            plugins,
            sourceMaps: needSourceMap,
            sourceFileName: id,
            configFile: false
            });
            if (!ssr && !needHmr) {
            if (!result.code) return;
            return {
            code: result.code,
            map: result.map
            };
            }
            const declaredComponents = [];
            const hotComponents = [];
            for (const node of result.ast.program.body) {
            if (node.type === "VariableDeclaration") {
            const names = parseComponentDecls(node, defineComponentName);
            if (names.length) declaredComponents.push(...names);
            }
            if (node.type === "ExportNamedDeclaration") {
            if (node.declaration && node.declaration.type === "VariableDeclaration") hotComponents.push(...parseComponentDecls(node.declaration, defineComponentName).map((name) => ({
            local: name,
            exported: name,
            id: getHash(id + name)
            })));
            else if (node.specifiers.length) {
            for (const spec of node.specifiers) if (spec.type === "ExportSpecifier" && spec.exported.type === "Identifier") {
            if (declaredComponents.find((name) => name === spec.local.name)) hotComponents.push({
            local: spec.local.name,
            exported: spec.exported.name,
            id: getHash(id + spec.exported.name)
            });
            }
            }
            }
            if (node.type === "ExportDefaultDeclaration") {
            if (node.declaration.type === "Identifier") {
            const _name = node.declaration.name;
            if (declaredComponents.find((name) => name === _name)) hotComponents.push({
            local: _name,
            exported: "default",
            id: getHash(id + "default")
            });
            } else if (isDefineComponentCall(unwrapTypeAssertion(node.declaration), defineComponentName)) hotComponents.push({
            local: "__default__",
            exported: "default",
            id: getHash(id + "default")
            });
            }
            }
            if (hotComponents.length) {
            if (needHmr && !ssr && !/\?vue&type=script/.test(id)) {
            let code = result.code;
            let callbackCode = ``;
            for (const { local, exported, id } of hotComponents) {
            code += `\n${local}.__hmrId = "${id}"\n__VUE_HMR_RUNTIME__.createRecord("${id}", ${local})`;
            callbackCode += `\n__VUE_HMR_RUNTIME__.reload("${id}", __${exported})`;
            }
            const newCompNames = hotComponents.map((c) => `${c.exported}: __${c.exported}`).join(",");
            code += `\nimport.meta.hot.accept(({${newCompNames}}) => {${callbackCode}\n})`;
            result.code = code;
            }
            if (ssr) {
            const normalizedId = normalizePath(path.relative(root, id));
            let ssrInjectCode = `\nimport { ssrRegisterHelper } from "${ssrRegisterHelperId}"\nconst __moduleId = ${JSON.stringify(normalizedId)}`;
            for (const { local } of hotComponents) ssrInjectCode += `\nssrRegisterHelper(${local}, __moduleId)`;
            result.code += ssrInjectCode;
            }
            }
            if (!result.code) return;
            return {
            code: result.code,
            map: result.map
            };
            }
          },
        },
      },
    ],
    resolve: {
      alias: {
        "@": "/src",
      },
    },
    css: {
      preprocessorOptions: {
        less: {
          additionalData: "@import \"@/styles/variables.less\";",
          javascriptEnabled: true,
        },
      },
    },
    mode: "development",
  },
}

导出的内容就是 vite.config.js 中配置信息

image.png

ModuleRunner模块运行器

  public async import<T = any>(url: string): Promise<T> {
    // 获取缓存模块
    const fetchedModule = await this.cachedModule(url)
    // 执行模块请求
    return await this.cachedRequest(url, fetchedModule)
  }

image.png

native (实验性)

native 模式直接通过 Node.js 原生的动态 import() 加载配置文件,不经过任何打包步骤。

只能编写纯 JavaScript,可以指定 --configLoader native 来使用环境的原生运行时加载配置文件。

{
   "start": "vite --configLoader=native",
}
  • 它的优点是简单快速,调试时断点可以直接定位到源码,不受临时文件干扰。
  • 但这种模式有一个重要限制:配置文件导入的模块的更新不会被检测到,因此不会自动重启 Vite 服务器
async function nativeImportConfigFile(resolvedPath: string) {
  const module = await import(
    pathToFileURL(resolvedPath).href + '?t=' + Date.now()
  )
  return {
    configExport: module.default,
    dependencies: [],
  }
}

在 native 模式下,由于没有经过打包工具分析依赖,Vite 无法知道配置文件引入了哪些本地模块。因此依赖列表被硬编码为空数组,意味着当配置文件导入的其他本地文件(如 ./utils.js)发生变化时,Vite 不会自动重启服务器。这是 native 模式的重要限制。

三者的区别

image.png

商务部等6部门:发展“人工智能+电商”,引导电商企业加强人工智能大模型等技术研发应用

2026年4月6日 10:11
据商务部网站,商务部等6部门发布关于更好服务实体经济、推进电子商务高质量发展的指导意见。意见提出,加快技术创新应用。支持头部电商企业加大研发投入,加强重点领域核心技术攻关,构建“产研协同、学用转化”创新生态。发展“人工智能+电商”,引导电商企业加强人工智能大模型等技术研发应用,优化消费体验、降低运营成本、提升流通效能。鼓励电商企业科技向善,统筹各方利益优化算法规则。加强电商领域技术成果司法保护,探索建立证据披露、证据妨碍排除等规则,适当减轻权利人举证负担。开展电商科技创新应用案例遴选,深化数字技术应用。(界面)

从零到一:在React前端中集成The Graph查询Uniswap V3池数据实战

作者 竹林818
2026年4月6日 10:01

从零到一:在React前端中集成The Graph查询Uniswap V3池数据实战

背景

上个月,我接了一个DeFi策略分析面板的前端开发需求。其中一个核心功能是展示Uniswap V3上特定交易对(比如ETH/USDC)的流动性池详情,包括当前价格、流动性总量、手续费率等。我的第一反应是直接用 ethers.jsviem 去读取对应的智能合约。这确实能行,我写了几个 readContract 调用,数据也拿到了。

但问题很快来了。当我想展示这个池子最近24小时的交易量变化,或者想列出这个池子所有大的流动性提供者(LP)时,直接查合约就变得非常笨重和低效。我需要遍历大量历史事件,这在浏览器端几乎不可能实现,而且会消耗天量的RPC请求。项目需要一个既能查询实时状态又能高效检索历史事件的解决方案。这时,我想到了 The Graph——一个专门用于索引和查询区块链数据的去中心化协议。理论上,我可以通过它订阅一个已经索引好的Uniswap V3子图,用GraphQL轻松拿到所有结构化数据。

问题分析

一开始,我以为集成The Graph会很简单:找个现成的Uniswap V3子图,用 fetchaxios 发个GraphQL请求不就完了?但上手后发现,事情没那么直白。

首先,我找到了Uniswap官方在The Graph托管服务上部署的V3子图。但我直接用自己的前端项目去请求它的公开端点时,遇到了CORS(跨域资源共享)错误。浏览器的安全策略阻止了我的本地开发服务器向 https://api.thegraph.com 发起请求。这是第一个拦路虎。

其次,即使CORS问题解决了,GraphQL查询的编写也让我有点懵。子图暴露的数据模式(Schema)和我直接从合约里读到的原始数据格式不一样,它是被索引和加工过的实体(Entities)。我需要搞清楚有哪些实体可用,以及它们之间的关联关系。

最后,我希望能有一个类型安全的开发体验。GraphQL查询返回的 any 类型在TypeScript项目里用着心里发虚,后期维护也容易出错。我需要一种方法能为查询结果生成明确的TypeScript接口。

最初的“简单fetch方案”显然走不通,我需要一个更正式、更健壮的前端集成方案。

核心实现

1. 选择客户端与绕过CORS

直接调用The Graph的公共HTTPS端点遇到CORS限制,这是前端开发中常见的问题。The Graph的托管服务默认可能没有配置允许所有来源。解决这个问题有几种思路:配置自己的代理服务器,或者使用支持自定义端点的Graph客户端库。

我选择了 Apollo Client。它是一个功能强大的GraphQL客户端,不仅帮我管理请求状态、缓存,更重要的是,它通常用于服务端渲染(SSR)或静态生成(SSG)场景,在这些场景中,请求发自Node.js环境而非浏览器,从而天然避开了CORS问题。对于我的纯前端项目,我可以先通过配置一个简单的本地开发代理来解决CORS问题,未来部署时可以考虑使用无服务器函数作为代理。

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

npm install @apollo/client graphql

然后,我创建了Apollo Client的实例,指向Uniswap V3在以太坊主网的子图端点。

// src/lib/apolloClient.ts
import { ApolloClient, InMemoryCache, HttpLink, from } from '@apollo/client';
import { onError } from '@apollo/client/link/error';

// 注意:在浏览器中直接使用此端点会因CORS失败
// 在开发环境中,我们需要配置代理或使用其他方法
const GRAPHQL_ENDPOINT = 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3';

const httpLink = new HttpLink({
  uri: GRAPHQL_ENDPOINT,
});

const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors)
    graphQLErrors.forEach(({ message, locations, path }) =>
      console.error(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`)
    );
  if (networkError) console.error(`[Network error]: ${networkError}`);
});

export const apolloClient = new ApolloClient({
  link: from([errorLink, httpLink]),
  cache: new InMemoryCache(),
});

这里有个坑:在本地开发时,如果你在浏览器控制台看到CORS错误,一个快速的解决方案是在 vite.config.tswebpack.config.js 中配置开发服务器代理,将 /subgraph 路径的请求转发到The Graph API。

// vite.config.ts 示例
export default defineConfig({
  // ... 其他配置
  server: {
    proxy: {
      '/subgraph-api': {
        target: 'https://api.thegraph.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/subgraph-api/, ''),
      },
    },
  },
});

然后,将 GRAPHQL_ENDPOINT 改为 ‘/subgraph-api/subgraphs/name/uniswap/uniswap-v3’。这样,浏览器请求的是同源地址,由开发服务器代为转发,就绕过了CORS。

2. 编写并执行GraphQL查询

接下来,我需要编写正确的GraphQL查询。我先去The Graph的Explorer查看了 uniswap-v3 子图的Schema。我找到了几个关键实体:Pool(流动性池)、Token(代币)、Swap(兑换事件)等。

我的目标是查询一个特定池子的基本信息。我知道Uniswap V3池子的合约地址是由两个代币地址和手续费层级(feeTier)共同决定的。但更方便的是,子图已经为每个池子生成了一个唯一的ID,通常是合约地址。所以,我可以直接用池子合约地址来查询。

我在项目中创建了一个GraphQL查询文件:

# src/graphql/queries/poolInfo.graphql
query GetPoolInfo($poolId: ID!) {
  pool(id: $poolId) {
    id
    token0 {
      id
      symbol
      name
      decimals
    }
    token1 {
      id
      symbol
      name
      decimals
    }
    feeTier
    liquidity
    sqrtPrice
    tick
    volumeUSD
    txCount
    # 当前价格,需要根据sqrtPrice和代币精度计算
    # 这里我们先取出来原始数据,在前端转换
  }
}

然后,在React组件中,我使用 @apollo/clientuseQuery hook来执行这个查询。我选择了一个知名的ETH/USDC 0.3%费率的池子地址作为示例。

// src/components/PoolInfo.tsx
import { useQuery, gql } from '@apollo/client';
import React from 'react';

// 使用gql标签定义查询
const GET_POOL_INFO = gql`
  query GetPoolInfo($poolId: ID!) {
    pool(id: $poolId) {
      id
      token0 {
        id
        symbol
        name
        decimals
      }
      token1 {
        id
        symbol
        name
        decimals
      }
      feeTier
      liquidity
      sqrtPrice
      tick
      volumeUSD
      txCount
    }
  }
`;

// 一个已知的ETH/USDC 0.3%池地址
const SAMPLE_POOL_ID = '0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8';

export const PoolInfo: React.FC = () => {
  const { loading, error, data } = useQuery(GET_POOL_INFO, {
    variables: { poolId: SAMPLE_POOL_ID },
  });

  if (loading) return <p>Loading pool data from The Graph...</p>;
  if (error) return <p>Error :( {error.message}</p>;

  const pool = data.pool;
  // 计算当前价格:价格 = (sqrtPrice^2) / 2^(192) * (10^decimals1 / 10^decimals0)
  // 简化处理:这里只展示一个概念
  const token0Decimals = pool.token0.decimals;
  const token1Decimals = pool.token1.decimals;

  return (
    <div>
      <h2>Pool: {pool.token0.symbol} / {pool.token1.symbol}</h2>
      <p>Fee Tier: {pool.feeTier / 10000}%</p>
      <p>Liquidity: {parseFloat(pool.liquidity).toLocaleString()}</p>
      <p>Volume (USD): ${parseFloat(pool.volumeUSD).toLocaleString(undefined, { maximumFractionDigits: 2 })}</p>
      <p>Transaction Count: {pool.txCount}</p>
      <p>Pool Contract: <code>{pool.id}</code></p>
    </div>
  );
};

注意这个细节sqrtPricetick 是Uniswap V3用于表示价格的核心变量。前端需要根据公式将它们转换为人类可读的价格。上面的计算只是示意,实际项目中需要实现精确的转换函数。

3. 实现类型安全(Codegen)

手动为GraphQL查询结果定义TypeScript接口非常繁琐且容易出错。我决定使用 GraphQL Code Generator 来自动完成这项工作。

首先,安装必要的开发依赖:

npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations

然后,创建配置文件 codegen.yml

# codegen.yml
overwrite: true
schema: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3'
documents: 'src/graphql/**/*.graphql'
generates:
  src/generated/graphql.ts:
    plugins:
      - 'typescript'
      - 'typescript-operations'
    config:
      skipTypename: false
      withHooks: true # 如果使用React,可以生成对应的hooks

package.json 中添加一个脚本:

"scripts": {
  "codegen": "graphql-codegen --config codegen.yml",
  "codegen:watch": "graphql-codegen --config codegen.yml --watch"
}

运行 npm run codegen 后,会在 src/generated/graphql.ts 中自动生成所有类型定义和可能的React Hooks。现在,我可以以完全类型安全的方式重写我的查询:

// src/components/PoolInfoTyped.tsx
import React from 'react';
import { useGetPoolInfoQuery } from '../generated/graphql'; // 自动生成的Hook
import { apolloClient } from '../lib/apolloClient';
import { ApolloProvider } from '@apollo/client';

const SAMPLE_POOL_ID = '0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8';

const PoolInfoInner: React.FC = () => {
  // 现在,`data`、`variables` 的类型都是自动推断的!
  const { loading, error, data } = useGetPoolInfoQuery({
    variables: { poolId: SAMPLE_POOL_ID },
  });

  if (loading) return <p>Loading (with types)...</p>;
  if (error) return <p>Error (with types): {error.message}</p>;
  // TypeScript知道`data.pool`可能为null,因为GraphQL查询可能返回空
  if (!data || !data.pool) return <p>No pool found.</p>;

  const pool = data.pool;
  return (
    <div>
      <h2>Pool: {pool.token0.symbol} / {pool.token1.symbol}</h2>
      <p>Pool ID: <code>{pool.id}</code></p>
      {/* 访问其他属性都有完整的类型提示 */}
    </div>
  );
};

// 需要在外层提供Apollo Client
export const PoolInfoTyped: React.FC = () => (
  <ApolloProvider client={apolloClient}>
    <PoolInfoInner />
  </ApolloProvider>
);

通过Codegen,我获得了完美的开发体验:自动补全、类型检查、以及查询字段变更时的编译时报错,大大提升了代码的可靠性和开发效率。

4. 处理分页与复杂查询

基础信息查询搞定后,我需要实现更复杂的功能,比如列出该池子最近的Swap事件。这类列表查询通常涉及分页。The Graph的子图查询支持经典的 firstskipwhere 过滤参数。

我编写了一个分页查询Swap事件的GraphQL:

# src/graphql/queries/poolSwaps.graphql
query GetPoolSwaps($poolId: ID!, $first: Int!, $skip: Int!) {
  swaps(
    where: { pool: $poolId }
    orderBy: timestamp
    orderDirection: desc
    first: $first
    skip: $skip
  ) {
    id
    timestamp
    transaction {
      id
    }
    sender
    recipient
    amount0
    amount1
    amountUSD
  }
}

在React组件中,我可以结合 useQuery 和分页状态(如当前页码)来动态获取数据。对于无限滚动或加载更多,Apollo Client的 fetchMore 函数非常好用。

// 使用 useQuery 的 fetchMore 示例片段
const { data, loading, fetchMore } = useGetPoolSwapsQuery({
  variables: {
    poolId: SAMPLE_POOL_ID,
    first: 10,
    skip: 0,
  },
});

const loadMore = () => {
  fetchMore({
    variables: {
      skip: data?.swaps.length || 0,
    },
    // 更新查询结果的方式
    updateQuery: (prev, { fetchMoreResult }) => {
      if (!fetchMoreResult) return prev;
      return {
        swaps: [...prev.swaps, ...fetchMoreResult.swaps],
      };
    },
  });
};

完整代码示例

以下是一个简化但可运行的React组件示例,集成了上述所有关键点(假设已配置代理解决CORS,并已运行Codegen生成类型)。

// src/App.tsx
import React from 'react';
import { ApolloProvider } from '@apollo/client';
import { apolloClient } from './lib/apolloClient';
import { PoolDashboard } from './components/PoolDashboard';

function App() {
  return (
    <ApolloProvider client={apolloClient}>
      <div className="App">
        <h1>Uniswap V3 Pool Dashboard (Powered by The Graph)</h1>
        <PoolDashboard />
      </div>
    </ApolloProvider>
  );
}

export default App;
// src/components/PoolDashboard.tsx
import React, { useState } from 'react';
import { useGetPoolInfoQuery, useGetPoolSwapsQuery } from '../generated/graphql';

const ETH_USDC_POOL = '0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8';
const PAGE_SIZE = 5;

export const PoolDashboard: React.FC = () => {
  // 查询池子基本信息
  const { data: poolData, loading: poolLoading, error: poolError } = useGetPoolInfoQuery({
    variables: { poolId: ETH_USDC_POOL },
  });

  // 查询Swap事件,带分页
  const [swapsSkip, setSwapsSkip] = useState(0);
  const {
    data: swapsData,
    loading: swapsLoading,
    error: swapsError,
    fetchMore,
  } = useGetPoolSwapsQuery({
    variables: {
      poolId: ETH_USDC_POOL,
      first: PAGE_SIZE,
      skip: swapsSkip,
    },
  });

  const handleLoadMore = () => {
    const currentLength = swapsData?.swaps.length || 0;
    fetchMore({
      variables: { skip: currentLength },
      updateQuery: (prev, { fetchMoreResult }) => {
        if (!fetchMoreResult) return prev;
        return {
          swaps: [...prev.swaps, ...fetchMoreResult.swaps],
        };
      },
    }).then(() => {
      setSwapsSkip(currentLength);
    });
  };

  if (poolLoading) return <div>Loading pool info...</div>;
  if (poolError) return <div>Error loading pool: {poolError.message}</div>;
  if (!poolData?.pool) return <div>Pool not found.</div>;

  const pool = poolData.pool;

  return (
    <div style={{ padding: '20px' }}>
      <section>
        <h2>
          {pool.token0.symbol} / {pool.token1.symbol} Pool (Fee: {pool.feeTier / 10000}%)
        </h2>
        <p>
          <strong>Liquidity:</strong> {parseInt(pool.liquidity).toLocaleString()}
        </p>
        <p>
          <strong>24h Volume USD:</strong> $
          {parseFloat(pool.volumeUSD).toLocaleString(undefined, {
            maximumFractionDigits: 0,
          })}
        </p>
      </section>

      <section style={{ marginTop: '40px' }}>
        <h3>Recent Swaps</h3>
        {swapsError && <p>Error loading swaps: {swapsError.message}</p>}
        {swapsLoading && <p>Loading swaps...</p>}
        <ul>
          {swapsData?.swaps.map((swap) => (
            <li key={swap.id} style={{ marginBottom: '10px', borderBottom: '1px solid #eee', paddingBottom: '5px' }}>
              <div>Tx: {swap.transaction.id.slice(0, 10)}...</div>
              <div>
                Amounts: {parseFloat(swap.amount0).toFixed(4)} {pool.token0.symbol} /{' '}
                {parseFloat(swap.amount1).toFixed(4)} {pool.token1.symbol}
              </div>
              <div>Value: ${parseFloat(swap.amountUSD).toFixed(2)}</div>
              <div>Time: {new Date(parseInt(swap.timestamp) * 1000).toLocaleString()}</div>
            </li>
          ))}
        </ul>
        <button onClick={handleLoadMore} disabled={swapsLoading}>
          {swapsLoading ? 'Loading...' : 'Load More Swaps'}
        </button>
      </section>
    </div>
  );
};

踩坑记录

  1. CORS错误:如前所述,在浏览器中直接调用The Graph托管服务API会遇到CORS。解决方法:在开发环境配置本地代理(如Vite的 server.proxy),在生产环境可以考虑使用Cloudflare Worker、AWS Lambda等无服务器函数作为代理,或者寻找支持CORS的公共网关(有些社区提供)。
  2. 查询返回 null:我传入一个正确的合约地址,但 pool 查询返回 null原因:子图索引的ID可能不是合约地址本身,而是小写格式。另外,有些池子可能因为索引延迟或尚未被索引而不存在。解决方法:确保ID格式正确(全小写),并检查子图是否已经同步到最新区块。可以在The Graph Explorer中先用相同ID测试查询。
  3. 类型生成失败:运行 graphql-codegen 时失败,报错“无法获取schema”。原因:网络问题或端点URL错误。解决方法:检查 codegen.yml 中的 schema URL是否正确且可访问。有时需要科学上网。也可以先将schema下载到本地文件,然后指向本地文件路径。
  4. 分页性能与 skip 限制:使用 skip 参数进行深度分页(例如 skip: 10000)在The Graph上可能非常慢甚至超时,因为底层数据库查询效率问题。解决方法:尽量避免大数值的 skip。推荐使用基于游标(cursor)的分页,即使用 where: { id_gt: $lastId }orderBy: id。但需要注意的是,这要求子图的Schema设计支持这种模式,并非所有查询都适用。

小结

这次实战让我彻底打通了从前端到链上索引数据的管道。The Graph + Apollo Client + GraphQL Codegen 的组合,为Web3前端提供了一套类型安全、高效且强大的数据查询方案。下一步,我计划深入研究子图的定义和部署,为自己项目的合约定制专属索引,从而解锁更复杂的数据展示和分析功能。

全国铁路今天预计发送旅客2080万人次

2026年4月6日 10:01
据国铁集团,今天(4月6日)全国铁路预计发送旅客2080万人次,计划加开旅客列车1369列。昨天(4月5日),全国铁路发送旅客1475.6万人次,运输安全平稳有序。(央视新闻)
❌
❌