阅读视图

发现新文章,点击刷新页面。

hexo-rs:玩 Vibe Coding

月底升级了 Copilot Pro+,月初额度重置,这几天可以放开用,想到什么就 vibe 一把。

我的博客跑在 Hexo 上很多年了。其实没什么大问题,就是每次看到那几百 MB 的 node_modules,心里总有点膈应——生成几百个静态 HTML,真的需要这么多依赖吗?但迁移到别的博客系统又懒得折腾,所以一直拖着。

这次干脆试试:能不能用 AI 一个下午撸一个 Rust 版的 Hexo?我的目标比较简单:生成跟原来一样的静态文件,兼容我现在用的主题就行。

我用的是 OpenCode + Opus 4.5。陆陆续续聊了一下午,产出了 hexo-rs。能用,但还有些边边角角的问题。

Vibe Coding 的工具和体会以后再写,这篇主要聊 hexo-rs 的实现和踩过的坑。

技术选型

EJS 模板引擎

Hexo 主题基本都用 EJS 模板——就是把 JavaScript 嵌到 HTML 里,跟 PHP 差不多。

QuickJS 跑 JS,通过 quick-js crate 调用。好处是不用依赖 Node.js,坏处是 Windows 上编不过(libquickjs-sys 挂了),所以暂时只支持 Linux 和 macOS。

其他

Markdown 用 pulldown-cmark,代码高亮用 syntect,本地服务器用 axum。都是常规选择,没什么特别的。

踩过的坑

HashMap 的坑

这个 bug 藏得很深。生成 tag 和 category 页面时,一开始用 HashMap 存文章分组:

let mut tags: HashMap<String, Vec<&Post>> = HashMap::new();

HashMap 迭代顺序不确定,每次生成的 HTML 可能不一样。页面看着没问题,但 diff 一下就发现乱了。改成 BTreeMap 就好了:

let mut tags: BTreeMap<String, Vec<&Post>> = BTreeMap::new();

Helper 函数

Hexo 有一堆 helper 函数:url_forcssjsdate 之类的。都得在 Rust 里实现一遍,然后塞进 QuickJS。

最烦的是 date。Hexo 用 Moment.js 的格式(YYYY-MM-DD),Rust 的 chrono 用 strftime(%Y-%m-%d)。得写个转换函数,挺无聊的活。

Partial 嵌套

EJS 的 partial 可以套娃,A 引用 B,B 又引用 C,变量还得一层层传下去。搞了个作用域栈,进 partial 压栈,出来弹栈。不难,但容易写错。

Vibe Coding 体感

代码 100% 是 AI 写的。我干的事:描述需求、review 代码、把报错贴给它让它改、偶尔拍板选方案。

像 EJS 模板引擎这种东西,自己从头写估计得半天,AI 几分钟就吐出来了。

但 AI 也挺蠢的:

  • HashMap 那个 bug 它就没注意到,我提出界面上的变化它也没反应过来
  • 一开始它写的 EJS parser 全是字符串 hardcode,丑得不行,我让它按 lexer -> AST 的套路重写了一遍
  • 代码多了以后它会忘事,前面写过的逻辑后面又写一遍

但 AI 又确实非常强,我想到应该使用现在线上的 catcoding.me 来和新生成的内容一一对比,然后它就呼啦啦地一通操作把问题都找出来了,自己修改完。

使用

cargo binstall hexo-rs  # 或 cargo install hexo-rs

hexo-rs generate  # 生成静态文件
hexo-rs server    # 本地预览
hexo-rs clean     # 清理
hexo-rs new "标题"

局限

不支持 Hexo 插件,不支持 Stylus 编译(.styl 文件得先用 Node 编译好),Windows 也不行。

简单的博客应该够用。复杂主题可能会有兼容问题。


代码在这:github.com/chenyukang/hexo-rs

用 Hexo 的可以试试。有问题提 issue,我让 AI 来修 :)

这篇文章到底是人写的,还是 AI 写的?

type-challenges(ts类型体操): 11 - 元组转换为对象

11 - 元组转换为对象

by sinoon (@sinoon) #简单 #object-keys

题目

将一个元组类型转换为对象类型,这个对象类型的键/值和元组中的元素对应。

例如:

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const

type result = TupleToObject<typeof tuple> // expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}

在 Github 上查看:tsch.js.org/11/zh-CN

代码

/* _____________ 你的代码 _____________ */

type TupleToObject<T extends readonly PropertyKey[]> = {
  [P in T[number]]: P
}

关键解释:

  • type PropertyKey = string | number | symbol
  • T extends readonly PropertyKey[] 用于限制 T 必须是一个只读的属性键元组。
  • [P in T[number]] 用于遍历元组中的每个元素,将其作为对象的键。
  • P 是元组中的元素类型,通过 T[number] 来获取。

相关知识点

extends

使用维度 核心作用 示例场景
类型维度 做类型约束或条件判断(类型编程核心) 限定泛型范围、判断类型是否兼容、提取类型片段
语法维度 做继承(复用已有结构) 接口继承、类继承
extends 做类型约束或条件判断
  1. 泛型约束:限定泛型的取值范围
// 约束 T 必须是「拥有 length 属性」的类型(比如 string/数组)
function getLength<T extends { length: number }>(arg: T): number {
  return arg.length;
}

// 合法调用(符合约束)
getLength("hello"); // ✅ string 有 length,返回 5
getLength([1, 2, 3]); // ✅ 数组有 length,返回 3

// 非法调用(超出约束)
getLength(123); // ❌ 报错:number 没有 length 属性
  1. 条件类型:类型版 三元运算符
// 基础示例:判断类型是否为字符串
type IsString<T> = T extends string ? true : false;

type A = IsString<"test">; // true(符合)
type B = IsString<123>; // false(不符合)

分布式条件类型(联合类型专用): 当 T 是联合类型时,extends 会自动拆分联合类型的每个成员,逐个判断后再合并结果。

type Union = string | number | boolean;

// 拆分逻辑:string→string,number→never,boolean→never → 合并为 string
type OnlyString<T> = T extends string ? T : never;
type Result = OnlyString<Union>; // Result = string

注意:只有泛型参数是 裸类型(没有被 []/{} 包裹)时,才会触发分布式判断:

// 包裹后不触发分布式,整体判断 [string|number] 是否兼容 [string]
type NoDist<T> = [T] extends [string] ? T : never;
type Result2 = NoDist<Union>; // never(整体不兼容)
  1. 配合 infer:提取类型片段(黄金组合)
// 提取 Promise 的返回值类型
type UnwrapPromise<T> = T extends Promise<infer V> ? V : T;

type C = UnwrapPromise<Promise<string>>; // string(提取成功)
type D = UnwrapPromise<number>; // number(不满足条件,返回原类型)
extends 做继承(复用已有结构)
  1. 接口继承:复用 + 扩展属性
// 基础接口
interface User {
  id: number;
  name: string;
}

// 继承 User,并扩展新属性
interface Admin extends User {
  role: "admin" | "super_admin"; // 新增权限属性
}

// 必须包含继承的 + 扩展的所有属性
const admin: Admin = {
  id: 1,
  name: "张三",
  role: "admin"
};

// 多接口继承
interface HasAge { age: number; }
interface Student extends User, HasAge {
  className: string; // 同时继承 User + HasAge
}
  1. 类继承:复用父类的属性 / 方法
class Parent {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  sayHi() {
    console.log(`Hi, ${this.name}`);
  }
}

// 继承 Parent 类
class Child extends Parent {
  age: number;
  constructor(name: string, age: number) {
    super(name); // 必须调用父类构造函数(初始化父类属性)
    this.age = age;
  }
  // 重写父类方法
  sayHi() {
    super.sayHi(); // 调用父类原方法
    console.log(`I'm ${this.age} years old`);
  }
}

const child = new Child("李四", 10);
child.sayHi(); // 输出:Hi, 李四 → I'm 10 years old

补充:类实现接口用 implements(不是 extends

// 定义接口(契约:规定必须有 id、name 属性,以及 greet 方法)
interface Person {
  id: number;
  name: string;
  greet(): void; // 仅定义方法签名,无实现
}

// 类实现接口(必须严格遵守契约)
class Employee implements Person {
  // 必须实现接口的所有属性
  id: number;
  name: string;

  // 构造函数初始化属性
  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }

  // 必须实现接口的 greet 方法(具体实现由类自己定义)
  greet() {
    console.log(`Hi, I'm ${this.name}, ID: ${this.id}`);
  }
}

// 实例化使用
const emp = new Employee(1, "张三");
emp.greet(); // 输出:Hi, I'm 张三, ID: 1


// 接口1:基础信息
interface Identifiable {
  id: number;
  getId(): number;
}

// 接口2:可打印
interface Printable {
  printInfo(): void;
}

// 类同时实现两个接口(必须实现所有接口的成员)
class Product implements Identifiable, Printable {
  id: number;
  name: string; // 类可扩展接口外的属性

  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }

  // 实现 Identifiable 的方法
  getId(): number {
    return this.id;
  }

  // 实现 Printable 的方法
  printInfo() {
    console.log(`Product: ${this.name}, ID: ${this.getId()}`);
  }
}

const product = new Product(100, "手机");
console.log(product.getId()); // 100
product.printInfo(); // Product: 手机, ID: 100

readonly

  • 核心作用:标记后,目标(属性 / 数组 / 元组)只能在初始化阶段赋值(比如接口实例化、类构造函数、变量声明时),后续任何修改运算都会被 TS 编译器拦截报错;
  • 运行时特性:readonly 仅做编译时检查,不会生成任何额外 JS 代码,也无法真正阻止运行时的修改(比如通过类型断言绕开的话,运行时仍能改);
  • const 的区别:const 是变量层面的不可重新赋值(但变量指向的对象 / 数组内部属性仍可改),readonly 是属性 / 类型层面的不可修改(变量本身可重新赋值,除非变量也用 const)。

常用使用场景:

  1. 作用于接口 / 类型别名的属性(最基础)
// 定义带只读属性的接口
interface User {
  readonly id: number; // 只读属性:只能初始化赋值,后续不可改
  name: string; // 普通属性:可修改
}

// 初始化时赋值(合法)
const user: User = { id: 1, name: "张三" };

// 尝试修改只读属性(报错)
user.id = 2; // ❌ 报错:无法分配到 "id",因为它是只读属性
// 修改普通属性(合法)
user.name = "李四"; // ✅ 合法
  1. 作用于类的属性: 类中使用 readonly 标记属性,只能在声明时构造函数中赋值,后续无法修改
class Person {
  readonly id: number; // 只读属性
  name: string;

  // 构造函数中给 readonly 属性赋值(唯一合法的后续赋值方式)
  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }

  updateInfo() {
    this.id = 100; // ❌ 报错:id 是只读属性
    this.name = "王五"; // ✅ 合法
  }
}

const person = new Person(1, "赵六");
person.id = 2; // ❌ 报错:只读属性不可修改
  1. 作用于数组 / 元组(只读数组): readonly 可标记数组为 “只读数组”,禁止修改数组元素、调用 push/pop 等修改方法
// 方式1:使用 readonly 修饰数组类型
const arr1: readonly number[] = [1, 2, 3];
arr1.push(4); // ❌ 报错:readonly 数组不存在 push 方法
arr1[0] = 10; // ❌ 报错:无法修改只读数组的元素

// 方式2:使用 ReadonlyArray<T> 类型(等价于 readonly T[])
const arr2: ReadonlyArray<string> = ["a", "b"];
arr2.pop(); // ❌ 报错

// 作用于元组(只读元组)
type Point = readonly [number, number];
const point: Point = [10, 20];
point[0] = 30; // ❌ 报错:只读元组元素不可修改
  1. 结合 keyof + in 批量创建只读类型(映射类型)
interface Product {
  name: string;
  price: number;
  stock: number;
}

// 批量创建只读版本的 Product(TS 内置的 Readonly<T> 就是这么实现的)
type ReadonlyProduct = {
  readonly [K in keyof Product]: Product[K];
};

const product: ReadonlyProduct = { name: "手机", price: 2999, stock: 100 };
product.price = 3999; // ❌ 报错:price 是只读属性

// TS 内置了 Readonly<T>,可直接使用(无需手动写映射类型)
const product2: Readonly<Product> = { name: "电脑", price: 5999, stock: 50 };
product2.stock = 60; // ❌ 报错
  1. 只读索引签名:如果类型使用索引签名,也可以标记为 readonly,禁止通过索引修改属性
// 只读索引签名:只能读取,不能修改
type ReadonlyDict = {
  readonly [key: string]: number;
};

const dict: ReadonlyDict = { a: 1, b: 2 };
dict["a"] = 3; // ❌ 报错:索引签名是只读的
console.log(dict["b"]); // ✅ 合法:仅读取

in

in 运算符用于遍历联合类型中的每个成员,将其转换为映射类型的属性名。

例如:

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoKeys = 'title' | 'description'

type TodoPreview = {
  [P in TodoKeys]: Todo[P]
}
// TodoPreview 类型为:
// {
//   title: string
//   completed: boolean
// }

T[number]

T[number] 索引访问类型 用于 从数组类型 / 元组类型中提取所有元素的类型,最终得到一个联合类型。

  1. 普通数组类型
// 定义普通数组类型
type StringArr = string[];
type NumberArr = number[];
type BoolArr = boolean[];

// T[number] 提取元素类型
type Str = StringArr[number]; // 结果:string
type Num = NumberArr[number]; // 结果:number
type Bool = BoolArr[number]; // 结果:boolean

// 等价于直接注解类型
let s: Str = "hello"; // 等同于 let s: string
let n: Num = 123;    // 等同于 let n: number
let b: Bool = true;  // 等同于 let b: boolean
  1. 元组类型
// 定义一个多类型的元组类型
type Tuple = [123, "TS", true, null];

// T[number] 提取所有元素的联合类型
type TupleUnion = Tuple[number]; // 结果:123 | "TS" | true | null

// 变量注解:可以是联合类型中的任意一种
let val: TupleUnion;
val = 123;    // 合法
val = "TS";   // 合法
val = true;   // 合法
val = null;   // 合法
val = false;  // ❌ 报错:不在联合类型中
  1. 字面量元组
// 字面量元组:元素是数字/字符串字面量
type StatusTuple = [200, 404, 500];
type EnvTuple = ["dev", "test", "prod"];

// 转字面量联合类型(开发中常用的枚举式类型)
type Status = StatusTuple[number]; // 结果:200 | 404 | 500
type Env = EnvTuple[number];       // 结果:"dev" | "test" | "prod"

// 严格限制变量值,避免手写错误
let code: Status = 200; // 合法
code = 404;             // 合法
code = 403;             // ❌ 报错:403 不在 200|404|500 中

let env: Env = "dev";   // 合法
env = "prod";           // 合法
env = "production";     // ❌ 报错:不在联合类型中
  1. as const + 数组 + T[number]

同时拥有数组的可遍历性 + 联合类型的严格类型约束。

// 步骤1:用 as const 断言数组为「只读字面量元组」
// 作用:让 TS 保留每个元素的字面量类型,且把数组转为只读元组(不可修改)
const EnvArr = ["dev", "test", "prod"] as const;
const StatusArr = [200, 404, 500] as const;

// 步骤2:用 typeof 获取数组的类型(只读字面量元组类型)
// 补充:typeof 是 TS 关键字,用于「从变量中提取其类型」
type EnvTuple = typeof EnvArr; // 类型:readonly ["dev", "test", "prod"]
type StatusTuple = typeof StatusArr; // 类型:readonly [200, 404, 500]

// 步骤3:用 T[number] 转成字面量联合类型
type Env = EnvTuple[number]; // 结果:"dev" | "test" | "prod"
type Status = StatusTuple[number]; // 结果:200 | 404 | 500

// 简化写法(开发中常用,省略中间元组类型)
type EnvSimplify = typeof EnvArr[number];
type StatusSimplify = typeof StatusArr[number];
  1. 泛型中使用 T[number]
// 泛型 T 约束为「只读数组」(兼容 as const 断言的数组)
function getUnionType<T extends readonly any[]>(arr: T): T[number] {
  return arr[Math.floor(Math.random() * arr.length)];
}

// 传入 as const 断言的数组,返回值自动推导为字面量联合类型
const res1 = getUnionType(["dev", "test", "prod"] as const); // res1 类型:"dev" | "test" | "prod"
const res2 = getUnionType([1, 2, 3] as const); // res2 类型:1 | 2 | 3

// 传入普通数组,返回值推导为基础类型
const res3 = getUnionType([1, 2, 3]); // res3 类型:number
  1. 支持嵌套数组 / 元组
const NestedArr = [[1, "a"], [2, "b"]] as const;
type NestedUnion = typeof NestedArr[number]; // 结果:readonly [1, "a"] | readonly [2, "b"]
type DeepUnion = typeof NestedArr[number][number]; // 结果:1 | "a" | 2 | "b"

测试用例

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
const tupleNumber = [1, 2, 3, 4] as const
const sym1 = Symbol(1)
const sym2 = Symbol(2)
const tupleSymbol = [sym1, sym2] as const
const tupleMix = [1, '2', 3, '4', sym1] as const

type cases = [
  Expect<Equal<TupleToObject<typeof tuple>, { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y' }>>,
  Expect<Equal<TupleToObject<typeof tupleNumber>, { 1: 1, 2: 2, 3: 3, 4: 4 }>>,
  Expect<Equal<TupleToObject<typeof tupleSymbol>, { [sym1]: typeof sym1, [sym2]: typeof sym2 }>>,
  Expect<Equal<TupleToObject<typeof tupleMix>, { 1: 1, '2': '2', 3: 3, '4': '4', [sym1]: typeof sym1 }>>,
]

// @ts-expect-error
type error = TupleToObject<[[1, 2], {}]>

相关链接

分享你的解答:tsch.js.org/11/answer/z… 查看解答:tsch.js.org/11/solution… 更多题目:tsch.js.org/zh-CN

下面是我的公众号,欢迎关注。关注后有新的功能点会及时收到推送。

实战为王!专注于汇总各种功能点,致力于打造一系列能够帮助工程师实现各种功能的想法思路的优质文章。

前端功能点

type-challenges(ts类型体操): 10 - 元组转合集

10 - 元组转合集

by Anthony Fu (@antfu) #中等 #infer #tuple #union

题目

实现泛型TupleToUnion<T>,它返回元组所有值的合集。

例如

type Arr = ['1', '2', '3']

type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3'

在 Github 上查看:tsch.js.org/10/zh-CN

代码

/* _____________ 你的代码 _____________ */

type TupleToUnion<T> = T extends [infer F, ...infer R] ? F | TupleToUnion<R> : never

关键解释:

  • T extends [infer F, ...infer R] 用于判断元组是否为空。
  • F | TupleToUnion<R> 用于递归处理元组的剩余部分。
  • never 用于处理空元组的情况。

相关知识点

extends

使用维度 核心作用 示例场景
类型维度 做类型约束或条件判断(类型编程核心) 限定泛型范围、判断类型是否兼容、提取类型片段
语法维度 做继承(复用已有结构) 接口继承、类继承
extends 做类型约束或条件判断
  1. 泛型约束:限定泛型的取值范围
// 约束 T 必须是「拥有 length 属性」的类型(比如 string/数组)
function getLength<T extends { length: number }>(arg: T): number {
  return arg.length;
}

// 合法调用(符合约束)
getLength("hello"); // ✅ string 有 length,返回 5
getLength([1, 2, 3]); // ✅ 数组有 length,返回 3

// 非法调用(超出约束)
getLength(123); // ❌ 报错:number 没有 length 属性
  1. 条件类型:类型版 三元运算符
// 基础示例:判断类型是否为字符串
type IsString<T> = T extends string ? true : false;

type A = IsString<"test">; // true(符合)
type B = IsString<123>; // false(不符合)

分布式条件类型(联合类型专用): 当 T 是联合类型时,extends 会自动拆分联合类型的每个成员,逐个判断后再合并结果。

type Union = string | number | boolean;

// 拆分逻辑:string→string,number→never,boolean→never → 合并为 string
type OnlyString<T> = T extends string ? T : never;
type Result = OnlyString<Union>; // Result = string

注意:只有泛型参数是 裸类型(没有被 []/{} 包裹)时,才会触发分布式判断:

// 包裹后不触发分布式,整体判断 [string|number] 是否兼容 [string]
type NoDist<T> = [T] extends [string] ? T : never;
type Result2 = NoDist<Union>; // never(整体不兼容)
  1. 配合 infer:提取类型片段(黄金组合)
// 提取 Promise 的返回值类型
type UnwrapPromise<T> = T extends Promise<infer V> ? V : T;

type C = UnwrapPromise<Promise<string>>; // string(提取成功)
type D = UnwrapPromise<number>; // number(不满足条件,返回原类型)
extends 做继承(复用已有结构)
  1. 接口继承:复用 + 扩展属性
// 基础接口
interface User {
  id: number;
  name: string;
}

// 继承 User,并扩展新属性
interface Admin extends User {
  role: "admin" | "super_admin"; // 新增权限属性
}

// 必须包含继承的 + 扩展的所有属性
const admin: Admin = {
  id: 1,
  name: "张三",
  role: "admin"
};

// 多接口继承
interface HasAge { age: number; }
interface Student extends User, HasAge {
  className: string; // 同时继承 User + HasAge
}
  1. 类继承:复用父类的属性 / 方法
class Parent {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  sayHi() {
    console.log(`Hi, ${this.name}`);
  }
}

// 继承 Parent 类
class Child extends Parent {
  age: number;
  constructor(name: string, age: number) {
    super(name); // 必须调用父类构造函数(初始化父类属性)
    this.age = age;
  }
  // 重写父类方法
  sayHi() {
    super.sayHi(); // 调用父类原方法
    console.log(`I'm ${this.age} years old`);
  }
}

const child = new Child("李四", 10);
child.sayHi(); // 输出:Hi, 李四 → I'm 10 years old

补充:类实现接口用 implements(不是 extends

// 定义接口(契约:规定必须有 id、name 属性,以及 greet 方法)
interface Person {
  id: number;
  name: string;
  greet(): void; // 仅定义方法签名,无实现
}

// 类实现接口(必须严格遵守契约)
class Employee implements Person {
  // 必须实现接口的所有属性
  id: number;
  name: string;

  // 构造函数初始化属性
  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }

  // 必须实现接口的 greet 方法(具体实现由类自己定义)
  greet() {
    console.log(`Hi, I'm ${this.name}, ID: ${this.id}`);
  }
}

// 实例化使用
const emp = new Employee(1, "张三");
emp.greet(); // 输出:Hi, I'm 张三, ID: 1


// 接口1:基础信息
interface Identifiable {
  id: number;
  getId(): number;
}

// 接口2:可打印
interface Printable {
  printInfo(): void;
}

// 类同时实现两个接口(必须实现所有接口的成员)
class Product implements Identifiable, Printable {
  id: number;
  name: string; // 类可扩展接口外的属性

  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }

  // 实现 Identifiable 的方法
  getId(): number {
    return this.id;
  }

  // 实现 Printable 的方法
  printInfo() {
    console.log(`Product: ${this.name}, ID: ${this.getId()}`);
  }
}

const product = new Product(100, "手机");
console.log(product.getId()); // 100
product.printInfo(); // Product: 手机, ID: 100

infer

infer 是 TypeScript 在条件类型中提供的关键字,用于声明一个 待推导的类型变量(类似给类型起一个临时名字),只能在 extends 子句中使用。它的核心作用是:从已有类型中提取 / 推导我们需要的部分,而无需手动硬编码类型。

infer 必须配合条件类型使用,语法结构如下:

// 基础结构:推导 T 的类型为 U,若能推导则返回 U,否则返回 never
type InferType<T> = T extends infer U ? U : never;

type Example = InferType<string>; // Example 类型为 string
type Example2 = InferType<number[]>; // Example2 类型为 number[]

高频使用场景:

1. 提取函数的返回值类型
// 定义类型工具:提取函数的返回值类型
type GetReturnType<Fn> = Fn extends (...args: any[]) => infer R ? R : never;

// 测试用函数
const add = (a: number, b: number): number => a + b;
const getUser = () => ({ name: "张三", age: 20 });

// 使用类型工具
type AddReturn = GetReturnType<typeof add>; // AddReturn 类型为 number
type UserReturn = GetReturnType<typeof getUser>; // UserReturn 类型为 { name: string; age: number }
2. 提取数组的元素类型
// 定义类型工具:提取数组元素类型
type GetArrayItem<T> = T extends (infer Item)[] ? Item : never;

// 测试
type NumberArray = GetArrayItem<number[]>; // NumberArray 类型为 number
type StringArray = GetArrayItem<string[]>; // StringArray 类型为 string
type MixedArray = GetArrayItem<[string, number]>; // MixedArray 类型为 string | number
3. 提取 Promise 的泛型参数类型
// 定义类型工具:提取 Promise 的泛型类型
type GetPromiseValue<T> = T extends Promise<infer Value> ? Value : never;

// 测试
type PromiseString = GetPromiseValue<Promise<string>>; // PromiseString 类型为 string
type PromiseUser = GetPromiseValue<Promise<{ id: number }>>; // PromiseUser 类型为 { id: number }
4. 提取函数的参数类型
// 定义类型工具:提取函数参数类型
type GetFunctionParams<Fn> = Fn extends (...args: infer Params) => any ? Params : never;

// 测试
const fn = (name: string, age: number): void => {};
type FnParams = GetFunctionParams<typeof fn>; // FnParams 类型为 [string, number]

// 进一步:提取第一个参数的类型
type FirstParam = GetFunctionParams<typeof fn>[0]; // FirstParam 类型为 string

|

| 运算符用于表示联合类型,即一个值可以是多个类型中的任意一个。

  1. 变量的联合类型注解
// 变量 a 可以是字符串 OR 数字
let a: string | number;

// 合法赋值(符合任意一种类型)
a = "TS";
a = 123;

// 非法赋值(不属于联合类型中的任何一种),TS 直接报错
a = true; // ❌ 类型 'boolean' 不能赋值给类型 'string | number'
  1. 函数参数的联合类型
// 函数接收 string 或 number 类型的参数
function printValue(val: string | number) {
  console.log(val);
}

// 合法调用
printValue("hello");
printValue(666);

// 非法调用,TS 报错
printValue(null); // ❌
  1. 数组的联合类型(注意两种写法的区别)
// 写法1:(A | B)[] —— 数组的「每个元素」可以是 A 或 B(混合数组)
let arr1: (string | number)[] = [1, "2", 3, "4"]; // 合法

// 写法2:A[] | B[] —— 「整个数组」要么全是 A 类型,要么全是 B 类型(纯数组)
let arr2: string[] | number[] = [1, 2, 3]; // 合法(全数字)
arr2 = ["1", "2", "3"]; // 合法(全字符串)
arr2 = [1, "2"]; // ❌ 报错:混合类型不符合要求

当使用联合类型的时候,访问某一个子类型的专属属性 / 方法时,需要进行类型守卫,可用的方法有 typeofinswitchinstanceof

  1. typeof
function getLength(val: string | number) {
  // 类型窄化:判断 val 是 string 类型
  if (typeof val === "string") {
    // 此分支中,TS 确定 val 是 string,可安全使用 length
    return val.length;
  } else {
    // 此分支中,TS 确定 val 是 number,执行数字相关逻辑
    return val.toString().length;
  }
}

console.log(getLength("TS")); // 2
console.log(getLength(1234)); // 4
  1. in
function printUserInfo(user: { name: string } | { age: number }) {
  // 类型窄化:判断 user 是否有 name 属性(即是否是 { name: string } 类型)
  if ("name" in user) {
    console.log(`Name: ${user.name}`);
  } else {
    // 此分支中,TS 确定 user 是 { age: number } 类型
    console.log(`Age: ${user.age}`);
  }
}
  1. switch
interface User {
  type: "user";
  name: string;
  age: number;
}
interface Admin {
  type: "admin";
  name: string;
  permission: string[];
}
// 联合类型:可以是 User 或 Admin
type Person = User | Admin;
function printPerson(p: Person) {
  switch (p.type) {
    case "user":
      console.log(p.age); // 确定是 User
      break;
    case "admin":
      console.log(p.permission); // 确定是 Admin
      break;
  }
}
  1. instanceof
// 定义两个类
class Dog {
  bark() { console.log("汪汪"); }
}
class Cat {
  meow() { console.log("喵喵"); }
}

// 联合类型:Dog 或 Cat 实例
type Animal = Dog | Cat;

// instanceof 类型守卫(针对类实例)
function animalCall(animal: Animal) {
  if (animal instanceof Dog) {
    animal.bark();
  } else {
    animal.meow();
  }
}

animalCall(new Dog()); // 汪汪
animalCall(new Cat()); // 喵喵

测试用例

/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<TupleToUnion<[123, '456', true]>, 123 | '456' | true>>,
  Expect<Equal<TupleToUnion<[123]>, 123>>,
]

相关链接

分享你的解答:tsch.js.org/10/answer/z… 查看解答:tsch.js.org/10/solution… 更多题目:tsch.js.org/zh-CN

下面是我的公众号,欢迎关注。关注后有新的功能点会及时收到推送。

实战为王!专注于汇总各种功能点,致力于打造一系列能够帮助工程师实现各种功能的想法思路的优质文章。

前端功能点

为网页注入灵魂:Live2D Widget看板娘,打造会动的互动伙伴!

厌倦了静态网页的冰冷与单调?Live2D Widget 能将一个生动、可爱的看板娘轻松带入你的网站。只需一行代码,这个由 TypeScript 驱动的开源项目即可为博客、个人主页或任何网页赋予灵动的生命。她不仅会眨眼、转头,还能与访客进行简单的互动,瞬间提升网站的趣味性与亲和力。无论是技术极客追求的可定制性,还是普通用户向往的轻松集成,Live2D Widget 都能满足。跟随本文,一分钟唤醒你的网页,让数字世界多一位温暖陪伴。

🎯项目介绍

  • • 页面互动:在网页中添加 Live2D 看板娘
  • • 易于集成: 核心代码由 TypeScript 编写,易于集成,只需一行代码,即可为网站添加看板娘
  • • 轻量级设计:除Live2D核心库外无额外依赖,加载迅速
  • • 高度可定制:支持多种配置选项,完美适配你的网站风格

github地址:github.com/stevenjoezh…

官网地址:www.live2d.com/en

该项目目前在github上已有 10.4k ⭐️ star

⚡快速开始:一分钟集成

对于大多数用户来说,集成过程简单得令人惊喜:

<!-- 只需在页面中添加这行代码 -->
<script src="https://fastly.jsdelivr.net/npm/live2d-widgets@1.0.0/dist/autoload.js"></script>

我的博客页面 xiuji008.github.io/ 已经集成了,家人们可以移步过去看看效果,以下是一些效果图

🎖️进阶

如果你是小白,我们通过上边介绍的那行代码就已经把看板娘集成进来了。但是如果你想让看板娘更适合你的网站,你可以通过进一步的配置及开发来完成,感兴趣的家人们可以自行研究。

相关技术博文:www.fghrsh.net/post/123.ht…

📝 总结:让网页活起来

Live2D Widget不仅仅是一个技术项目,它代表了网页交互的新可能。在这个数字化的时代,一个灵动的看板娘能够:

  • • 提升用户体验和停留时间
  • • 增加网站的特色和记忆点
  • • 展示技术实力和创意精神

无论你是技术爱好者、博主还是网站开发者,Live2D Widget都能为你的项目增添独特的魅力。立即尝试,让你的网站拥有一个随时陪伴访客的可爱伙伴吧!

让技术更有温度,让网页更有生命!🚀

大模型发展史-01

前言

2017年,一篇论文悄然发表,题为《Attention Is All You Need》。

当时没人预料到,这篇论文中提出的 Transformer 架构,会在短短几年内彻底改变人工智能的格局。

五年后的2022年11月30日,ChatGPT 发布。五天内,用户突破100万。两个月内,用户突破1亿。

这是互联网历史上增长最快的应用,也是人工智能发展史上的重要里程碑。

从默默无闻到席卷全球,大语言模型经历了怎样的进化之路?让我们一起回顾这段激动人心的技术演进史。


1. 什么是 Transformer

Transformer 是一种完全基于注意力机制的神经网络架构,于2017年由 Google 团队提出。

核心创新

特点 说明
Self-Attention 自注意力机制,捕捉长距离依赖
并行计算 可并行训练,大幅提升效率
可扩展性 为后续大模型奠定基础

核心思想

// Transformer 的核心:Self-Attention
class Transformer {
  attention(Q, K, V) {
    // Q (Query)、K (Key)、V (Value)
    const scores = Q @ K.T / Math.sqrt(d_k);  // 计算注意力分数
    const weights = softmax(scores);           // 归一化
    return weights @ V;                        // 加权求和
  }
}

重要术语

术语 解释
预训练 用大量无标注数据训练基础模型
微调 针对特定任务用小数据集优化模型
RLHF 人类反馈强化学习,对齐人类偏好
少样本学习 只需几个例子就能学会新任务

2. 案例

案例 1:GPT 系列的进化之路

让我们看看 GPT 系列是如何一步步进化的:

代际 发布时间 参数量 能力突破
GPT-1 2018.06 117M 预训练范式
GPT-2 2019.02 1.5B 零样本生成
GPT-3 2020.05 175B 少样本学习
GPT-3.5 2022.11 未知 对话能力
GPT-4 2023.03 ~1.7T 多模态+推理
GPT-4o 2024.05 未知 原生多模态

关键突破:GPT-3 的少样本学习

const prompt = `
翻译以下句子成中文:
Example 1: Hello world -> 你好世界
Example 2: How are you -> 你好吗
Input: Good morning -> ?
`;
// GPT-3: 早上好
// 没有专门训练,就能学会翻译任务

案例 2:ChatGPT 的 AI iPhone 时刻

发布时间:2022年11月30日

突破性改进

训练流程:
1. 预训练(学习知识)
   ↓
2. 有监督微调(学习指令)
   ↓
3. 奖奖模型(学习人类偏好)
   ↓
4. 强化学习(优化输出)

成果

  • 对话能力大幅提升
  • 指令遵循能力强
  • 多轮对话流畅
  • 5天用户破100万

案例 3:2023年百花齐放

闭源模型三强鼎立

模型 公司 核心优势
GPT-4 OpenAI 多模态、推理能力强
Claude 3 Anthropic 超长上下文(200K)
Gemini Google 原生多模态

开源模型快速追赶

模型 组织 参数 特点
Llama 3 Meta 8B/70B 性能强劲
Qwen 阿里云 7B/14B/72B 中文优秀
Mistral Mistral AI 7B 效率之王

中国大模型崛起

模型 公司 特色
文心一言 百度 知识图谱增强
通义千问 阿里云 开源友好
讯飞星火 科大讯飞 语音能力强
DeepSeek 幻方量化 性价比高

案例 4:2024年的三大趋势

趋势1:开源模型追平闭源

2024年初:Llama 2 70B  GPT-3.5
2024年中:Llama 3 70B 接近 GPT-4
2024年底:Qwen 2.5、DeepSeek V3 追平闭源

趋势2:多模态成为标配

  • GPT-4o:原生多模态
  • Claude 3.5:强大的视觉能力
  • Gemini:从一开始就是多模态

趋势3:智能体技术成熟

// Agent 能力的进化
2022:简单对话
2023:工具调用
2024:
  ├── 复杂任务规划
  ├── 多智能体协作
  ├── 自主学习和改进
  └── 真正的"AI 员工"

总结

  1. 规模即质量——更大的模型通常表现更好
  2. 数据是关键——高质量训练数据至关重要
  3. 架构创新——Transformer 是核心突破
  4. 开源加速——开源模型推动技术普及

什么是大语言模型-00

前言

你有没有想过,当你问 ChatGPT 一个问题时,它是如何"思考"并给出回答的?

今天天气怎么样?——抱歉,我无法获取实时天气信息。 请用 JavaScript 写一个快速排序——几秒钟内,代码就出现在屏幕上。

同样是 AI,为什么能写代码却不能查天气?大语言模型的"知识"从哪里来?它是真的"理解"我们的话吗?

这些问题,正是我们探索大语言模型(Large Language Model,LLM)世界的起点。


1. 什么是大语言模型

大语言模型(LLM) 是一种经过海量文本数据训练的深度学习模型,能够理解和生成人类语言。

关键特征

特征 说明 例子
大规模训练 使用 TB 级文本数据 GPT-4 训练了约 1 万亿 tokens
深度神经网络 数十亿到数万亿参数 GPT-3 有 1750 亿参数
通用能力 不需要专门训练就能完成多种任务 翻译、写作、编程、推理

通俗理解

想象一下:

  • 你阅读了互联网上几乎所有的文本
  • 你记住了其中的模式、规律和知识
  • 当有人问你问题时,你能根据记忆生成回答

这就是大语言模型做的事情!

核心工作原理

LLM 的本质是一个文字接龙机器

输入: "今天天气"
LLM 预测下一个词可能是:
- "真好"    (概率 30%)
- "很热"    (概率 25%)
- "怎么样"  (概率 20%)

训练流程

┌─────────────────────────────────────────┐
│            LLM 训练流程                   │
├─────────────────────────────────────────┤
│                                         │
│  1. 数据收集                             │
│     ├── 网页文本                         │
│     ├── 书籍文章                         │
│     └── 代码库                           │
│                                         │
│  2. 预训练                               │
│     ├── 学习语言模式                     │
│     ├── 学习世界知识                     │
│     └── 学习逻辑推理                     │
│                                         │
│  3. 微调                                 │
│     ├── 对齐人类偏好                     │
│     ├── 遵循指令                         │
│     └── 安全性训练                       │
│                                         │
└─────────────────────────────────────────┘

四大核心能力

1. 语言理解

  • 理解文本含义
  • 识别情感倾向
  • 提取关键信息

2. 语言生成

  • 写文章、写代码
  • 创意写作
  • 总结提炼

3. 逻辑推理

  • 数学计算
  • 逻辑推理
  • 问题解决

4. 少样本学习

  • 看几个例子就能学会新任务
  • 不需要重新训练

2. 案例

案例 1:少样本学习的神奇之处

让我们看看 LLM 如何通过几个例子学会新任务:

const prompt = `
例子1:
输入:苹果
输出:水果

例子2:
输入:胡萝卜
输出:蔬菜

输入:香蕉
输出:?
`;
// LLM 能推断:香蕉 → 水果

image.png

解析:无需重新训练,只需几个示例,LLM 就能理解分类规律并应用到新问题。

案例 2:代码生成能力

输入:"请用 JavaScript 写一个快速排序"

LLM 输出

function quickSort(arr) {
  if (arr.length <= 1) {
    return arr;
  }
  const pivot = arr[Math.floor(arr.length / 2)];
  const left = arr.filter(x => x < pivot);
  const middle = arr.filter(x => x === pivot);
  const right = arr.filter(x => x > pivot);
  return [...quickSort(left), ...middle, ...quickSort(right)];
}

解析:LLM 从训练数据中学会了编程模式和算法逻辑,能够生成可运行的代码。

案例 3:发现 LLM 的局限性

测试 1:实时信息

用户: "今天天气怎么样?"
LLM: "抱歉,我无法获取实时天气信息。"

测试 2:精确计算

用户: "12345 × 67890 = ?"
LLM: "大约是 83,000,000 左右"
实际: 838,102,050

测试 3:知识截止

用户: "谁赢得了2024年奥运会?"
LLM: "抱歉,我的知识截止到2023年..."

解析:这些测试揭示了 LLM 的三大局限——知识截止、幻觉问题、无法访问实时信息。

案例 4:实际项目中的调用

在本项目的后端代码中,LLM 调用是这样实现的:

async chat(request: {
  question: string;    // 用户的问题
  model: string;       // 使用的模型(如 qwen-plus)
  apiKey: string;      // API 密钥
}) {
  // 调用阿里云百炼的 LLM
  const response = await axios.post(
    'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
    {
      model: request.model,
      messages: [{ role: 'user', content: request.question }]
    }
  );

  return response.data.choices[0].message.content;
}

解析:通过 HTTP API 调用,将用户问题发送给 LLM,获取生成的回复。


总结

  1. LLM 是文字接龙机器——核心原理是预测下一个词
  2. LLM 有强大但有限的能力——理解、生成、推理、学习都很强,但并非万能
  3. LLM 的知识来自训练数据——它学习的是模式和规律,而非简单记忆
  4. LLM 会犯错——幻觉、知识截止、计算不精确是常见问题

Flutter最佳实践:Sliver族网络刷新组件NCustomScrollView

一、需求来源

最近需要实现嵌套和吸顶Header滚动下的下拉刷新及上拉加载。最终实现基于 CustomScrollView 的刷新视图组件。

simulator_screenshot_5C4883E4-F919-4FFD-BE3D-97E0BCD5C40D.png

二、使用示例

Widget buildBodyNew() {
  return NCustomScrollView<String>(
    onRequest: (bool isRefresh, int page, int pageSize, pres) async {
      final length = isRefresh ? 0 : pres.length;
      final list = List<String>.generate(pageSize, (i) => "item${length + i}");
      DLog.d([isRefresh, list.length]);
      return list;
    },
    headerSliverBuilder: (context, bool innerBoxIsScrolled) {
      return [
        buildPersistentHeader(),
      ];
    },
    itemBuilder: (_, i, e) {
      return ListTile(
        title: Text('Item $i'),
      );
    },
  );
}

三、源码

//
//  NCustomScrollView.dart
//  projects
//
//  Created by shang on 2026/1/28 14:41.
//  Copyright © 2026/1/28 shang. All rights reserved.
//

import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter_templet_project/basicWidget/n_placeholder.dart';
import 'package:flutter_templet_project/basicWidget/n_sliver_decorated.dart';
import 'package:flutter_templet_project/basicWidget/refresh/easy_refresh_mixin.dart';
import 'package:flutter_templet_project/basicWidget/refresh/n_refresh_view.dart';

/// 基于 CustomScrollView 的下拉刷新,上拉加载更多的滚动列表
class NCustomScrollView<T> extends StatefulWidget {
  const NCustomScrollView({
    super.key,
    this.title,
    this.placeholder = const NPlaceholder(),
    this.contentDecoration = const BoxDecoration(),
    this.contentPadding = const EdgeInsets.all(0),
    required this.onRequest,
    required this.headerSliverBuilder,
    required this.itemBuilder,
    this.separatorBuilder,
    this.headerBuilder,
    this.footerBuilder,
    this.builder,
  });

  final String? title;

  final Widget? placeholder;

  final Decoration contentDecoration;

  final EdgeInsets contentPadding;

  /// 请求方法
  final RequestListCallback<T> onRequest;

  /// 列表表头
  final NestedScrollViewHeaderSliversBuilder? headerSliverBuilder;

  /// ListView 的 itemBuilder
  final ValueIndexedWidgetBuilder<T> itemBuilder;

  final IndexedWidgetBuilder? separatorBuilder;

  /// 列表表头
  final List<Widget> Function(int count)? headerBuilder;

  /// 列表表尾
  final List<Widget> Function(int count)? footerBuilder;

  final Widget Function(List<T> items)? builder;

  @override
  State<NCustomScrollView<T>> createState() => _NCustomScrollViewState<T>();
}

class _NCustomScrollViewState<T> extends State<NCustomScrollView<T>>
    with AutomaticKeepAliveClientMixin, EasyRefreshMixin<NCustomScrollView<T>, T> {
  @override
  bool get wantKeepAlive => true;

  final scrollController = ScrollController();

  @override
  late RequestListCallback<T> onRequest = widget.onRequest;

  @override
  List<T> items = <T>[];

  @override
  void didUpdateWidget(covariant NCustomScrollView<T> oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.title != oldWidget.title ||
        widget.placeholder != oldWidget.placeholder ||
        widget.contentDecoration != oldWidget.contentDecoration ||
        widget.contentPadding != oldWidget.contentPadding ||
        widget.onRequest != oldWidget.onRequest ||
        widget.itemBuilder != oldWidget.itemBuilder ||
        widget.separatorBuilder != oldWidget.separatorBuilder) {
      setState(() {});
    }
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    if (items.isEmpty) {
      return GestureDetector(onTap: onRefresh, child: Center(child: widget.placeholder));
    }

    final child = EasyRefresh.builder(
      controller: refreshController,
      onRefresh: onRefresh,
      onLoad: onLoad,
      childBuilder: (_, physics) {
        return CustomScrollView(
          physics: physics,
          slivers: [
            ...(widget.headerBuilder?.call(items.length) ?? []),
            buildContent(),
            ...(widget.footerBuilder?.call(items.length) ?? []),
          ],
        );
      },
    );
    if (widget.headerSliverBuilder == null) {
      return child;
    }

    return NestedScrollView(
      headerSliverBuilder: widget.headerSliverBuilder!,
      body: child,
    );
  }

  Widget buildContent() {
    if (items.isEmpty) {
      return SliverToBoxAdapter(child: widget.placeholder);
    }

    return NSliverDecorated(
      decoration: widget.contentDecoration,
      sliver: SliverPadding(
        padding: widget.contentPadding,
        sliver: widget.builder?.call(items) ?? buildSliverList(),
      ),
    );
  }

  Widget buildSliverList() {
    return SliverList.separated(
      itemBuilder: (_, i) => widget.itemBuilder(context, i, items[i]),
      separatorBuilder: (_, i) => widget.separatorBuilder?.call(context, i) ?? const SizedBox(),
      itemCount: items.length,
    );
  }
}

源码:EasyRefreshMixin.dart

//
//  EasyRefreshMixin.dart
//  projects
//
//  Created by shang on 2026/1/28 14:37.
//  Copyright © 2026/1/28 shang. All rights reserved.
//

import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter_templet_project/basicWidget/refresh/n_refresh_view.dart';

/// EasyRefresh刷新 mixin
mixin EasyRefreshMixin<W extends StatefulWidget, T> on State<W> {
  late final refreshController = EasyRefreshController(
    controlFinishRefresh: true,
    controlFinishLoad: true,
  );


  /// 请求方式
  late RequestListCallback<T> _onRequest;
  RequestListCallback<T> get onRequest => _onRequest;
  set onRequest(RequestListCallback<T> value) {
    _onRequest = value;
  }

  // 数据列表
  List<T> _items = [];
  List<T> get items => _items;
  set items(List<T> value) {
    _items = value;
  }

  int page = 1;
  final int pageSize = 20;
  var indicator = IndicatorResult.success;

  @override
  void dispose() {
    refreshController.dispose();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();

    WidgetsBinding.instance.addPostFrameCallback((_) {
      // DLog.d([widget.title, widget.key, hashCode]);
      if (items.isEmpty) {
        onRefresh();
      }
    });
  }

  Future<void> onRefresh() async {
    try {
      page = 1;
      final list = await onRequest(true, page, pageSize, <T>[]);
      items.replaceRange(0, items.length, list);
      page++;

      final noMore = list.length < pageSize;
      if (noMore) {
        indicator = IndicatorResult.noMore;
      }
      refreshController.finishRefresh();
      refreshController.resetFooter();
    } catch (e) {
      refreshController.finishRefresh(IndicatorResult.fail);
    }
    setState(() {});
  }

  Future<void> onLoad() async {
    if (indicator == IndicatorResult.noMore) {
      refreshController.finishLoad();
      return;
    }

    try {
      final start = (items.length - pageSize).clamp(0, pageSize);
      final prePages = items.sublist(start);
      final list = await onRequest(false, page, pageSize, prePages);
      items.addAll(list);
      page++;

      final noMore = list.length < pageSize;
      if (noMore) {
        indicator = IndicatorResult.noMore;
      }
      refreshController.finishLoad(indicator);
    } catch (e) {
      refreshController.finishLoad(IndicatorResult.fail);
    }
    setState(() {});
  }
}

最后、总结

1、当页面比较复杂,需要吸顶或者嵌套滚动时就必须使用 Sliver 相关组件,否则会有滚动行文冲突。

2、NCustomScrollView 支持顶部吸顶组件自定义;底部列表头,列表尾设置,支持sliver 设置 Decoration。

3、支持下拉刷新,上拉加载更多,代码极简,使用方便。

4、刷新逻辑封装在 EasyRefreshMixin 混入里,方便多组件可共用。

github

追觅科技亮相2026中英企业家委员会会议

1月29日,2026中英企业家委员会会议在京举行,追觅科技作为行业代表受邀参会。追觅科技创立于2017年,目前业务已覆盖全球超120个国家和地区,开设超6500家旗舰店。在英国市场,其扫地机品类市占率近30%,伯明翰旗舰店位于核心商圈New Street。此次受邀参与会议,彰显其全球化实力与品牌国际影响力,将进一步助力其全球智能生态布局。

荃银高科:涉嫌信息披露违法违规,被中国证监会立案调查

36氪获悉,荃银高科公告,公司于2026年1月30日收到中国证监会下发的《立案告知书》,因公司涉嫌信息披露违法违规,中国证监会决定对公司立案。目前公司生产经营正常,立案调查不会对公司正常生产经营活动产生重大影响。公司将积极配合中国证监会的工作,严格履行信息披露义务。

莫高股份:公司股票可能被实施退市风险警示

36氪获悉,莫高股份公告,经公司财务部门初步测算,预计公司2025年年度利润总额、净利润或者扣除非经常性损益后的净利润孰低者为负值,且扣除与主营业务无关的业务收入和不具备商业实质的收入后的营业收入低于3亿元。公司股票在2025年年度报告披露后可能被实施退市风险警示(在公司股票简称前冠以“*ST”字样)。

flutter添加间隙gap源码解析

Flutter 小部件,可轻松在 Flex 小部件(如列和行)或滚动视图中添加间隙。

Gap的核心原理是使用RenderObject自定义实现布局。

Gap


class Gap extends StatelessWidget {
  const Gap(
    this.mainAxisExtent, {
    Key? key,
    this.crossAxisExtent,
    this.color,
  })  : assert(mainAxisExtent >= 0 && mainAxisExtent < double.infinity),
        assert(crossAxisExtent == null || crossAxisExtent >= 0),
        super(key: key);

  const Gap.expand(
    double mainAxisExtent, {
    Key? key,
    Color? color,
  }) : this(
          mainAxisExtent,
          key: key,
          crossAxisExtent: double.infinity,
          color: color,
        );

  final double mainAxisExtent;

  final double? crossAxisExtent;

  final Color? color;

  @override
  Widget build(BuildContext context) {
    final scrollableState = Scrollable.maybeOf(context);
    final AxisDirection? axisDirection = scrollableState?.axisDirection;
    final Axis? fallbackDirection =
        axisDirection == null ? null : axisDirectionToAxis(axisDirection);

    return _RawGap(
      mainAxisExtent,
      crossAxisExtent: crossAxisExtent,
      color: color,
      fallbackDirection: fallbackDirection,
    );
  }
}

从源码看, 它提供了两个构造方法, GapGap.expand方便用户按需使用。

_RawGap

_RawGap是核心类, 它继承了LeafRenderObjectWidget.

class _RawGap extends LeafRenderObjectWidget {
  const _RawGap(
    this.mainAxisExtent, {
    Key? key,
    this.crossAxisExtent,
    this.color,
    this.fallbackDirection,
  })  : assert(mainAxisExtent >= 0 && mainAxisExtent < double.infinity),
        assert(crossAxisExtent == null || crossAxisExtent >= 0),
        super(key: key);

  final double mainAxisExtent;

  final double? crossAxisExtent;

  final Color? color;

  final Axis? fallbackDirection;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderGap(
      mainAxisExtent: mainAxisExtent,
      crossAxisExtent: crossAxisExtent ?? 0,
      color: color,
      fallbackDirection: fallbackDirection,
    );
  }

  @override
  void updateRenderObject(BuildContext context, RenderGap renderObject) {
    if (kDebugMode) {
      debugPrint(
        '[Gap] updateRenderObject '
        'mainAxisExtent=$mainAxisExtent '
        'crossAxisExtent=${crossAxisExtent ?? 0} '
        'color=$color '
        'fallbackDirection=$fallbackDirection',
      );
    }
    renderObject
      ..mainAxisExtent = mainAxisExtent
      ..crossAxisExtent = crossAxisExtent ?? 0
      ..color = color
      ..fallbackDirection = fallbackDirection;
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DoubleProperty('mainAxisExtent', mainAxisExtent));
    properties.add(
        DoubleProperty('crossAxisExtent', crossAxisExtent, defaultValue: 0));
    properties.add(ColorProperty('color', color));
    properties.add(EnumProperty<Axis>('fallbackDirection', fallbackDirection));
  }
}

RenderGap

class RenderGap extends RenderBox {
  RenderGap({
    required double mainAxisExtent,
    double? crossAxisExtent,
    Axis? fallbackDirection,
    Color? color,
  })  : _mainAxisExtent = mainAxisExtent,
        _crossAxisExtent = crossAxisExtent,
        _color = color,
        _fallbackDirection = fallbackDirection {
    if (kDebugMode) {
      debugPrint(
        '🆕 RenderGap<init> '
        'mainAxisExtent=$mainAxisExtent '
        'crossAxisExtent=$crossAxisExtent '
        'color=$color '
        'fallbackDirection=$fallbackDirection',
      );
    }
  }

  double get mainAxisExtent => _mainAxisExtent;
  double _mainAxisExtent;
  set mainAxisExtent(double value) {
    if (_mainAxisExtent != value) {
      if (kDebugMode) {
        debugPrint('📏 mainAxisExtent set: $_mainAxisExtent -> $value');
      }
      _mainAxisExtent = value;
      markNeedsLayout();
    }
  }

  double? get crossAxisExtent => _crossAxisExtent;
  double? _crossAxisExtent;
  set crossAxisExtent(double? value) {
    if (_crossAxisExtent != value) {
      if (kDebugMode) {
        debugPrint('📐 crossAxisExtent set: $_crossAxisExtent -> $value');
      }
      _crossAxisExtent = value;
      markNeedsLayout();
    }
  }

  Axis? get fallbackDirection => _fallbackDirection;
  Axis? _fallbackDirection;
  set fallbackDirection(Axis? value) {
    if (_fallbackDirection != value) {
      if (kDebugMode) {
        debugPrint('🧭 fallbackDirection set: $_fallbackDirection -> $value');
      }
      _fallbackDirection = value;
      markNeedsLayout();
    }
  }

  Axis? get _direction {
    final parentNode = parent;
    if (parentNode is RenderFlex) {
      return parentNode.direction;
    } else {
      return fallbackDirection;
    }
  }

  Color? get color => _color;
  Color? _color;
  set color(Color? value) {
    if (_color != value) {
      if (kDebugMode) {
        debugPrint('🎨 color set: $_color -> $value');
      }
      _color = value;
      markNeedsPaint();
    }
  }

  @override
  double computeMinIntrinsicWidth(double height) {
    final result = _computeIntrinsicExtent(
      Axis.horizontal,
      () => super.computeMinIntrinsicWidth(height),
    )!;
    if (kDebugMode) {
      debugPrint('🔹 computeMinIntrinsicWidth(height=$height) => $result');
    }
    return result;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    final result = _computeIntrinsicExtent(
      Axis.horizontal,
      () => super.computeMaxIntrinsicWidth(height),
    )!;
    if (kDebugMode) {
      debugPrint('🔷 computeMaxIntrinsicWidth(height=$height) => $result');
    }
    return result;
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    final result = _computeIntrinsicExtent(
      Axis.vertical,
      () => super.computeMinIntrinsicHeight(width),
    )!;
    if (kDebugMode) {
      debugPrint('🔸 computeMinIntrinsicHeight(width=$width) => $result');
    }
    return result;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    final result = _computeIntrinsicExtent(
      Axis.vertical,
      () => super.computeMaxIntrinsicHeight(width),
    )!;
    if (kDebugMode) {
      debugPrint('🔶 computeMaxIntrinsicHeight(width=$width) => $result');
    }
    return result;
  }

  double? _computeIntrinsicExtent(Axis axis, double Function() compute) {
    final Axis? direction = _direction;
    if (direction == axis) {
      final result = _mainAxisExtent;
      if (kDebugMode) {
        debugPrint(
          '📐 _computeIntrinsicExtent(axis=$axis, direction=$direction) => $result',
        );
      }
      return result;
    } else {
      if (_crossAxisExtent!.isFinite) {
        final result = _crossAxisExtent;
        if (kDebugMode) {
          debugPrint(
            '📐 _computeIntrinsicExtent(axis=$axis, direction=$direction) => $result',
          );
        }
        return result;
      } else {
        final result = compute();
        if (kDebugMode) {
          debugPrint(
            '📐 _computeIntrinsicExtent(axis=$axis, direction=$direction) => $result',
          );
        }
        return result;
      }
    }
  }

  @override
  Size computeDryLayout(BoxConstraints constraints) {
    final Axis? direction = _direction;

    if (direction != null) {
      if (direction == Axis.horizontal) {
        final s =
            constraints.constrain(Size(mainAxisExtent, crossAxisExtent!));
        if (kDebugMode) {
          debugPrint(
            '💧 computeDryLayout(constraints=$constraints, direction=$direction) => $s',
          );
        }
        return s;
      } else {
        final s =
            constraints.constrain(Size(crossAxisExtent!, mainAxisExtent));
        if (kDebugMode) {
          debugPrint(
            '💧 computeDryLayout(constraints=$constraints, direction=$direction) => $s',
          );
        }
        return s;
      }
    } else {
      throw FlutterError(
        'A Gap widget must be placed directly inside a Flex widget '
        'or its fallbackDirection must not be null',
      );
    }
  }

  @override
  void performLayout() {
    size = computeDryLayout(constraints);
    if (kDebugMode) {
      debugPrint('🛠 performLayout(constraints=$constraints) size=$size');
    }
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (color != null) {
      final Paint paint = Paint()..color = color!;
      context.canvas.drawRect(offset & size, paint);
      if (kDebugMode) {
        debugPrint('🎨 paint(offset=$offset, size=$size, color=$color)');
      }
    } else {
      if (kDebugMode) {
        debugPrint('🎨 paint(offset=$offset, size=$size, color=null)');
      }
    }
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    if (kDebugMode) {
      debugPrint('🧾 debugFillProperties()');
    }
    properties.add(DoubleProperty('mainAxisExtent', mainAxisExtent));
    properties.add(DoubleProperty('crossAxisExtent', crossAxisExtent));
    properties.add(ColorProperty('color', color));
    properties.add(EnumProperty<Axis>('fallbackDirection', fallbackDirection));
  }
}

截屏2026-01-30 19.30.39.png

真正绘制原理, RenderGapRenderBox的子类, 不需要子类, 绘制时, 只与自身sizecolor有关。

mainAxisExtentcrossAxisExtent属性set方法触发后, 会执行markNeedsLayout, 标记该渲染对象需要重新布局, 并请求(requestVisualUpdate)调度下一帧执行布局。

布局阶段,会执行computeDryLayoutperformLayout方法,更新size

绘制阶段paint,在 offset & size 的矩形内填充颜色(color 为 null 时不绘制)。

  • 矩形范围:offset & size 等价于 Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height),保证绘制严格位于本组件区域。
  • 无子节点与图层:RenderGap 不 push 额外 Layer,也不绘制子内容;仅把一个矩形指令提交到当前画布。

markNeedsLayout布局阶段不同的是, markNeedsPaint绘制阶段不参与尺寸计算, 它在size确定后才执行。

与标记方法的关系:

  • markNeedsPaint:当 color 变更时由属性 setter 调用,标记本节点需要在下一帧重绘;不会触发布局。
  • markNeedsLayout:当 mainAxisExtent/crossAxisExtent/fallbackDirection 变更引起尺寸或方向变化时调用;下一帧会重新布局,布局完成后若绘制区域或内容也需更新才会出现 paint。
  • 执行链路示例:属性变更 → 标记(layout/paint)→ 布局(computeDryLayout/performLayout)→ 绘制(paint)。

新易盛:2025年净利同比预增231.24%-248.86%

36氪获悉,新易盛发布2025年业绩预告。报告显示,预计2025年归属于上市公司股东的净利润为94亿元-99亿元,比上年同期增长231.24%-248.86%。报告期内,受益于算力投资持续增长,高速率产品需求快速提升,公司销售收入和净利润大幅增加。

市场监管总局公布4起直播电商领域典型案例,成都快购被罚超2600万

36氪获悉,市场监管总局今天(30日)集中发布第五批直播电商领域典型案例。其中,市场监管总局查处成都快购科技有限公司违法案。2025年12月,市场监管总局综合考量当事人的违法事实、案件性质、违法情节以及社会危害程度,依法对当事人的违法行为作出行政处罚。责令当事人立即改正违法行为,并处罚没款26692904.62元。

上纬新材:2025年净利润同比减少37%-54%

36氪获悉,上纬新材发布2025年业绩预告。报告显示,预计2025年年度实现归属于母公司所有者的净利润与上年同期(法定披露数据)相比,将减少3,300万元到4,800万元,同比减少37%到54%。上年同期归属于母公司所有者的净利润为8,868.14万元。净利润较上年同期减少主要系新材料业务加大研发投入、计提投资损失和信用减值损失、探索新方向增加费用投入等。

顾地科技:预计2025年度期末净资产为负值,公司股票交易可能被实施退市风险警示

36氪获悉,顾地科技公告,预计2025年归属于上市公司股东的净利润为亏损3亿元-5.77亿元,预计2025年度期末净资产为负值。若公司2025年度经审计的期末净资产为负值,根据相关规定,公司股票交易将在2025年年度报告披露后被实施退市风险警示(股票简称前冠以“*ST”字样)。

中国车企和特斯拉的下一战,战场已定

出品|虎嗅汽车组

作者|王亚骏

头图|视觉中国



在发布了“建设富足非凡世界”的新使命后,特斯拉又用两款旗舰车型,来为这个新使命“祭旗”。


在Q4财报电话会开场白环节上,特斯拉CEO马斯克表示,是时候让Model S和Model X“光荣退役(honorable discharge)”了,这两款特斯拉的功勋车型将于下季度基本停产。


Model S/X的成功,为特斯拉走辆车型Model 3/Y带来了研发资金。同时,它们还极大程度上促进了新能源汽车从"环保政策工具"向"大型科技类消费品"的跃迁。


原本生产Model S和Model X的弗里蒙特工厂,将改为生产Optimus机器人。这款产品可以视为特斯拉新使命中最为重要的一环,马斯克认为未来特斯拉80%的市值都由Optimus机器人支撑。不过,他的梦想正面临着来自大洋彼岸的威胁。


在财报电话会上,马斯克直言,中国擅长AI,也擅长制造,在人形机器人领域,特斯拉最大的竞争肯定来自中国。

 

从中国车企在人形机器人领域的动作来看,马斯克这番话并非什么“高情商”之语或杞人忧天。目前在国内,不管是新势力还是传统车企,均已在机器人产业进行了布局。


同时,中国车企的入场步伐也在不断加快。在特斯拉发布财报的三天前,理想汽车开了一次线上全员会,公司CEO李想表示,理想一定会做人形机器人,并会尽快让该产品落地亮相。



这意味着,在FSD尚未入华、双方智驾战还是“隔海叫阵”的情况下,中国车企和特斯拉的下一战已经确定将在机器人领域展开。


中国车企和特斯拉,为何同时盯上了人形机器人?当下国内汽车行业的内卷式竞争,对战局又将有何影响?


低投入+高回报预期,让中美车企同时盯上这块蛋糕


车企做人形机器人,很难算“跨界”。


首先,车企人形机器人专门搭建新的研发和制造团队。何小鹏曾表示,“车企70%的技术储备能直接复用到机器人身上。”


两者的技术复用度之所以如此之高,原因是在感知、决策、执行三大核心环节,智能汽车和人形机器人的技术架构高度重叠,甚至可以被视为“同一套技术栈的双线部署”。以Optimus机器人为例:


  • 感知层,Optimus机器人直接使用了FSD(特斯拉完全自动驾驶)的纯视觉感知方案;

  • 决策层,两者的算法相似度60%(华西证券测算);

  • 执行层,Cybertruck的线控转向与Optimus关节驱动存在复用。


除了软件外,两者在硬件上也有着极高的关联度,比如传感器、芯片、雷达、摄像头等,这进一步推高了供应链重合度。


中国电动汽车百人会副理事长兼秘书长张永伟认为,在供应链方面,智能汽车与人形机器人的重合度超过了60%(车企纷纷布局的飞行汽车也是同理)。


同时,在制造方面,两者装配逻辑、质量控制体系等方面也具备一定的通用性。这意味着,车企完全可以将自己过剩的汽车生产线改装成人形机器人生产线,无需再找地建厂。


如此高的关联度,让汽车不必因拓展人形机器人业务而损耗太多元气。而机器人市场的广阔前景,更是让车企在当下的布局有望变成一个“低成本、高回报”的投资。


据摩根士丹利预测,到了2050年,全球机器人销售额达到25万亿美元,是2025年销售额的250倍。而在未来的25年,汽车销量恐怕难有如此规模的增长。


不只有中国车企和特斯拉外想瓜分这片淘金地。日韩和欧洲车企也在人形领域进行了布局,其中进展最快的是现代汽车。 2021年6月,现代汽车收购了知名人形机器人公司波士顿动力。


不过在财报电话会上,马斯克并未提及丰田现代们。而在两年前,他曾公开预测,未来全球只有十家车企能存活,分别是九家中国车企和特斯拉。


这场仗会怎么打?


从各家车企在机器人量产方面的规划来看,中国车企和特斯拉之间的“机器人战争”极有可能在2027年被点燃。


在财报电话会上,马斯克表示特斯拉“可能会在几个月内发布Optimus 3。”在瑞士达沃斯世界经济论坛上,特斯拉CEO埃隆·马斯克宣布,公司计划于2027年底前向公众销售其Optimus人形机器人。


在大洋彼岸,奇瑞已经开始交付首款人形机器人“墨茵”(2025年交付量超300台)。同时,小鹏汽车也计划在2026年年底实现人形机器人的量产。


小鹏汽车人形机器人IRON


在这场竞争中,双方各有各的优势


得益于长期积累的自动驾驶AI算法和庞大的真实世界数据(数百万辆特斯拉汽车每天采集的端到端驾驶视频数据),特斯拉在技术上实现了领先。


马斯克在财报电话会上详细解释了自身技术和数据带来的产品优势。目前,人形机器人有三大难点:打造像人手一样灵活的机械手(手是机器人身上最难的部分),其次是现实世界AI,最后是规模化生产。这三点是目前最大的难题,“我认为特斯拉是唯一一家同时具备这三大要素的公司。”


面对特斯拉的技术壁垒,中国车企并非没有还手之力。在智能汽车的竞争中,中国车企已经证明了自己的快速迭代和成本控制能力。摩根士丹利曾发布研报表示,中国在人形机器人产业供应链中占据主导地位,占比达到63%,“中国庞大的市场规模正在把人形机器人的制造成本打下来”。


在这种局面下,谁能吸引到更多更好的人才,谁的获胜概率就更大。何小鹏曾表示,吸引最顶尖人才,是做好人形机器人的关键。


不过,当下汽车行业的价格战和内卷,一定程度上扯了中国车企招募人才的后腿。


在线上全员会中,李想在谈及人形机器人业务时表示,公司要招聘这方面最好的人才,要从机器人创业公司那里挖人。


当下,车企和机器人公司之间的人才流动的确存在。但虎嗅汽车在与多位行业人士交流中发现,这种流动的主要方向却并非从机器人公司流向车企,而是反过来,从车企“逃到”机器人公司。


车企员工们之所以选择逃离,重要原因之一便是为了躲避内卷。同时,由于汽车和机器人,他们跳槽后的学习和适应成本也比较低。


与之对比,虽然特斯拉在2025年遭遇了业绩下滑,马斯克形象受损等风波,但这家新能源汽车先行者仍对人才保持了不俗的吸引力。据雇主品牌调研机构Universum调研,在2025年美国最具吸引力雇主排名中,特斯拉位列工程类学生就业选择第九名,是排名最高的汽车制造商(马斯克一直强调特斯拉是AI公司而非汽车公司,不过在2025年,汽车业务的收入仍占公司总收入的73.4%,为696亿美元)。


从这方面来看,当下的汽车行业内卷,难以对人形机器人业务起到什么正面作用。好在,中国车企与特斯拉的机器人之战,也并非短时间内便会走到决战时刻。

下载虎嗅APP,第一时间获取深度独到的商业科技资讯,连接更多创新人群与线下活动

万科:我爸又来救我了,羡慕吗?

喜大普奔!万科终于开始还钱了,这段时间万科还活着吗?郁亮是真退休还是被带走了?万科是不是要爆雷了?万科的房子还能不能买?这些问题在评论区反复出现,而这些问题在今天都有了阶段性的答案,我将用三期视频将这些坑全部填上。

下载虎嗅APP,第一时间获取深度独到的商业科技资讯,连接更多创新人群与线下活动

❌