普通视图

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

每日一题-三段式数组 I🟢

2026年2月3日 00:00

给你一个长度为 n 的整数数组 nums

如果存在索引 0 < p < q < n − 1,使得数组满足以下条件,则称其为 三段式数组(trionic)

  • nums[0...p] 严格 递增,
  • nums[p...q] 严格 递减,
  • nums[q...n − 1] 严格 递增。

如果 nums 是三段式数组,返回 true;否则,返回 false

 

示例 1:

输入: nums = [1,3,5,4,2,6]

输出: true

解释:

选择 p = 2, q = 4

  • nums[0...2] = [1, 3, 5] 严格递增 (1 < 3 < 5)。
  • nums[2...4] = [5, 4, 2] 严格递减 (5 > 4 > 2)。
  • nums[4...5] = [2, 6] 严格递增 (2 < 6)。

示例 2:

输入: nums = [2,1,3]

输出: false

解释:

无法选出能使数组满足三段式要求的 pq

 

提示:

  • 3 <= n <= 100
  • -1000 <= nums[i] <= 1000

Three.js 适配的“保姆级”教程,拒绝拉伸和模糊!

作者 烛阴
2026年2月2日 23:44

第一步:CSS 铺满全屏

首先,我们要让 Canvas 元素本身在网页里占满位置。这步最简单,但是很关键,很多人容易漏掉 display: block

默认情况下,Canvas 是 inline 元素,这会导致它底部莫名其妙多出几像素的“留白”(就像文字的行高一样)。

✅ 正确的 CSS 写法:

html, body {
  margin: 0;
  height: 100%;
}

#c {
  width: 100%;  /* 宽度占满父容器 */
  height: 100%; /* 高度占满父容器 */
  display: block; /* 关键!去除底部留白 */
}

第二步:拒绝“暴力拉伸”

CSS 把 Canvas 拉大了,但 Canvas 内部的“绘图缓冲区”(Drawing Buffer)可能还停留在原来的大小。

这就好比你把一张 100x100 的低清图片,强行用 CSS 放大到 1920x1080 显示,结果必然是模糊马赛克

在渲染循环中检查 Three.js 官方推荐写一个通用函数,在每一帧渲染前检查 Canvas 的显示大小和实际大小是否一致。

function resizeRendererToDisplaySize(renderer) {
  const canvas = renderer.domElement;
  // 获取 Canvas 在屏幕上显示的像素宽
  const width = canvas.clientWidth;
  // 获取 Canvas 在屏幕上显示的像素高
  const height = canvas.clientHeight;
  
  // 检查渲染器的内部尺寸是否和显示尺寸一样
  const needResize = canvas.width !== width || canvas.height !== height;
  
  if (needResize) {
    // 如果不一样,就调整渲染器大小
    // 第三个参数 false 很重要!表示不要让渲染器去修改 Canvas 的 CSS 样式
    renderer.setSize(width, height, false);
  }
  
  return needResize;
}

第三步:告诉摄像机“别把人拍扁了”(校准)

解决了模糊问题,还有一个变形问题。

想象一下,你的显示器本来是宽屏(16:9),摄像机也是按 16:9 拍摄的。当你把窗口缩窄变成竖屏(9:16)时,如果摄像机不调整,它还是把 16:9 的画面强行塞进 9:16 的格子里,物体就会被横向挤压

所以,每当分辨率改变时,我们必须更新摄像机的 长宽比 (Aspect Ratio)

✅ 最终的渲染循环代码:

function render(time) {
  time *= 0.001;

  // 1. 检查并调整分辨率
  if (resizeRendererToDisplaySize(renderer)) {
    const canvas = renderer.domElement;
    
    // 2. 修正摄像机长宽比
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    
    // 3. 必须调用这个方法,摄像机参数才会生效!
    camera.updateProjectionMatrix();
  }

  // 正常渲染
  renderer.render(scene, camera);
  
  requestAnimationFrame(render);
}

requestAnimationFrame(render);

📂 核心代码与完整示例:   my-three-app

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多Three.js开发干货

type-challenges(ts类型体操): 16 - 排除最后一项

作者 fxss
2026年2月2日 22:26

16 - 排除最后一项

by Anthony Fu (@antfu) #中等 #array

题目

在此挑战中建议使用TypeScript 4.0

实现一个泛型Pop<T>,它接受一个数组T,并返回一个由数组T的前 N-1 项(N 为数组T的长度)以相同的顺序组成的数组。

例如

type arr1 = ['a', 'b', 'c', 'd']
type arr2 = [3, 2, 1]

type re1 = Pop<arr1> // expected to be ['a', 'b', 'c']
type re2 = Pop<arr2> // expected to be [3, 2]

额外:同样,您也可以实现ShiftPushUnshift吗?

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

代码

/* _____________ 你的代码 _____________ */

type Pop<T extends any[]> = T extends [] ? [] : T extends [...infer A, infer _] ? A : never

关键解释:

  • T extends any[]:约束 T 必须是数组类型。
  • T extends [...infer A, infer _]:模式匹配,捕获数组最后一个元素之外的所有前置元素。
  • A:前置元素数组。
  • _:数组最后一个元素。

相关知识点

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

...infer

type Pop<T extends any[]> = T extends [] ? [] : T extends [...infer A, infer _] ? A : never

type Res1 = Pop<[1,2,3]> // 结果:[1,2]
type Res2 = Pop<[string]> // 推导:A = [],_ = string → 返回 []
type Res3 = Pop<[]> // 结果:[]

这里的 ...infer A 不是值层面的剩余操作符,而是 TS 数组 / 元组类型的模式匹配语法,它放在第一个位置是为了匹配并捕获数组最后一个元素之外的所有前置元素

// Shift 类型:移除第一个元素,返回剩余部分
type Shift<T extends any[]> = T extends [] ? [] : T extends [infer _, ...infer A] ? A : never

type Res4 = Shift<[1,2,3]> // 结果:[2,3]
type Res5 = Shift<[string]> // 结果:[]

...infer A 放到最后,infer _ 放到第一个,模式就变成了 [infer _, ...infer A],此时匹配的是 固定第一个元素 + 任意后置剩余元素

// 移除数组最后两个元素,返回前置部分
type Pop2<T extends any[]> = T extends [...infer A, infer _, infer __] ? A : T

type Res6 = Pop2<[1,2,3,4]> // 结果:[1,2]
type Res7 = Pop2<[1,2]> // 结果:[]
type Res8 = Pop2<[1]> // 不匹配模式,返回原类型 [1]

测试用例

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

type cases = [
  Expect<Equal<Pop<[3, 2, 1]>, [3, 2]>>,
  Expect<Equal<Pop<['a', 'b', 'c', 'd']>, ['a', 'b', 'c']>>,
  Expect<Equal<Pop<[]>, []>>,
]

相关链接

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

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

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

Vue-常用修饰符

2026年2月2日 21:49

前言

在 Vue 开发中,修饰符(Modifiers)是指令后的一个特殊后缀(以 . 开头),它能以极简的方式帮我们处理事件冒泡、键盘监听以及复杂的双向绑定逻辑。掌握它们,能让你的模板代码既优雅又高效。

一、 事件修饰符:精准控制交互行为

事件修饰符主要用于处理 DOM 事件的细节。

  • .stop:阻止事件冒泡(调用 event.stopPropagation())。
  • .prevent:阻止事件的默认行为(调用 event.preventDefault())。
  • .capture:在捕获模式下触发事件监听器。
  • .self:只有当事件是从触发元素本身触发时才触发回调。
  • .once:事件只触发一次,之后自动移除监听器。
  • .passive:滚动事件的性能优化,告诉浏览器不需要等待 preventDefault

二、 键盘与鼠标修饰符:语义化监听

1. 按键修饰符

在监听键盘事件时,我们经常需要检查特定的按键,例如:<input @keyup.enter="submitForm" type="text" placeholder="按回车提交">

  • .enter:回车键
  • .tab:Tab 键
  • .space:空格键
  • .delete:删除或退格键
  • .up / .down / .left / .right:方向键

2. 鼠标修饰符

用于限制处理程序仅响应特定的鼠标按键。

  • .left:点击鼠标左键触发。
  • .right:点击鼠标右键触发。
  • .middle:点击鼠标中键(滚轮点击)触发。

三、 v-model 修饰符:数据预处理

这些修饰符可以自动处理表单输入的数据格式。

  • .lazy: 将v-model的同步时机设置在change事件之后,一般为在输入框失去焦点时。
  • .number:自动将用户的输入值转为数值类型(内部使用 parseFloat)。
  • .trim:自动过滤用户输入内容的首尾空白字符。

四、 双向绑定修饰符

这是 Vue 2 到 Vue 3 变化最大的部分。

1. Vue 2 时代的 .sync

在 Vue 2 中,.sync 是实现父子组件属性双向绑定的语法糖。

// 使用 .sync 的语法糖
<ChildComponent :title.sync="pageTitle" />
// 在子组件的方法中
this.$emit('update:title', newTitleValue);

2. Vue 3 的统一:v-model:prop

Vue 3 废弃了 .sync,将其功能合并到了 v-model 中。支持在同一个组件上绑定多个 v-model

 // 在父组件中
<ChildComponent v-model:title="pageTitle" />

// 子组件
<script setup>
defineProps(['title']);
const emit = defineEmits(['update:title']);

const updateTitle = (newVal) => {
  emit('update:title', newVal);
};
</script>

3. Vue 3.4+ 的黑科技:defineModel

这是目前 Vue 3 最推荐的写法,极大简化了双向绑定的逻辑代码。

// 父组件
<ChildComponent v-model="inputValue" />

// 子组件
const inputValue = defineModel({
 // inputValue为双向绑定输入框的值
  type: [String],
  // 默认值
  default: ''
})

五、 总结

  1. 交互逻辑优先使用事件修饰符,减少组件内的非业务代码。
  2. 表单处理善用 .trim.number,降低后端校验压力。
  3. 父子通信在 Vue 3 项目中全面拥抱 v-model:prop,如果是新项目(Vue 3.4+),请直接使用 defineModel,它能让你的代码量减少 50% 以上。

Pagefind:为静态网站打造的极速搜索方案

作者 jump_jump
2026年2月2日 21:04

Pagefind 是一个专为静态网站设计的开源搜索引擎,它能够自动索引你的网站并提供完全离线的搜索体验。

核心特性

  • 按需加载:只下载搜索相关的内容片段,而不是整个索引
  • 轻量级:核心 JS 仅约 20KB,索引文件高度压缩(相比 Lunr.js 减少 85%)
  • 零配置:自动识别内容,开箱即用
  • 多语言支持:内置中文、日文等多语言分词器
  • 完全静态:无需服务器端支持,支持完全离线

快速上手

三步启用搜索

# 1. 构建你的静态网站
npm run build

# 2. 生成搜索索引
npx pagefind --source "dist"

# 3. 在 HTML 中添加搜索界面
<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
<div id="search"></div>
<script src="/pagefind/pagefind-ui.js"></script>
<script>
    new PagefindUI({ element: "#search" });
</script>

Pagefind 会自动在 dist/pagefind/ 目录下生成索引文件。

核心用法

控制索引范围

使用 data-pagefind-body 标记要索引的内容:

<main data-pagefind-body>
    <h1>文章标题</h1>
    <p>这部分内容会被索引</p>
</main>

<!-- 使用 data-pagefind-ignore 排除特定内容 -->
<div data-pagefind-ignore>
    <h2>评论</h2>
    <div class="comments">...</div>
</div>

添加元数据和权重

<!-- 自定义元数据 -->
<article data-pagefind-body
         data-pagefind-meta="author:张三,date:2024-01-01">
    <h1 data-pagefind-weight="10">文章标题</h1>
    <p data-pagefind-weight="5">摘要内容...</p>
    <div>正文内容...</div>
</article>

配置文件

# pagefind.yml
source: "dist"
exclude_selectors:
  - "nav"
  - ".sidebar"
force_language: "zh-cn"

自定义搜索 UI

import * as pagefind from '/pagefind/pagefind.js';

const search = await pagefind.search("React");
const results = await Promise.all(
    search.results.map(r => r.data())
);

实战指南

集成到构建流程

{
  "scripts": {
    "build": "vite build",
    "postbuild": "pagefind --source dist"
  }
}

React 自定义搜索组件

import { useState } from 'react';

function Search() {
    const [results, setResults] = useState([]);

    const handleSearch = async (e) => {
        const { default: pagefind } = await import('/pagefind/pagefind.js');
        const search = await pagefind.search(e.target.value);
        const data = await Promise.all(
            search.results.slice(0, 5).map(r => r.data())
        );
        setResults(data);
    };

    return (
        <>
            <input type="search" onChange={handleSearch} />
            {results.map((r, i) => (
                <a key={i} href={r.url}>
                    <h3>{r.meta.title}</h3>
                    <p dangerouslySetInnerHTML={{ __html: r.excerpt }} />
                </a>
            ))}
        </>
    );
}

最佳实践

1. 只索引主要内容

<!-- ✅ 推荐 -->
<main data-pagefind-body>
    <article>...</article>
</main>

2. 使用权重优化结果

<h1 data-pagefind-weight="10">标题</h1>
<p data-pagefind-weight="5">摘要</p>

3. CLI 参数配置

# 排除选择器
pagefind --source "dist" --exclude-selectors "nav" --exclude-selectors "footer"

# 强制语言
pagefind --source "dist" --force-language "zh-cn"

配置参考

HTML 属性

属性 说明
data-pagefind-body 标记要索引的主要内容区域
data-pagefind-ignore 排除该元素及其子元素
data-pagefind-meta 添加自定义元数据
data-pagefind-filter 定义可过滤的字段
data-pagefind-sort 定义可排序的字段
data-pagefind-weight 设置内容权重(1-10)

JavaScript API

// 高级搜索
const search = await pagefind.search("React", {
  filters: { category: "tutorial" },
  sort: { date: "desc" },
  limit: 10
});

// 获取结果
const results = await Promise.all(
  search.results.map(r => r.data())
);

原理深度解析

整体架构

首先通过架构图了解 Pagefind 的整体设计:

graph TB
    subgraph "构建阶段 Build Time"
        A[HTML 文件] --> B[内容扫描器]
        B --> C[内容提取器]
        C --> D[多语言分词器]
        D --> E[倒排索引构建器]
        E --> F[索引分片器]
        F --> G[压缩引擎]
        G --> H[索引文件]
    end

    subgraph "运行阶段 Runtime"
        I[用户查询] --> J[查询分词]
        J --> K[哈希计算]
        K --> L[按需加载器]
        H --> L
        L --> M[索引查询]
        M --> N[TF-IDF 评分]
        N --> O[结果排序]
        O --> P[内容片段加载]
        P --> Q[摘要生成]
        Q --> R[搜索结果]
    end

    subgraph "缓存层 Cache Layer"
        S[浏览器缓存]
        T[内存缓存]
        L -.-> S
        L -.-> T
    end

    style A fill:#e1f5ff
    style H fill:#e1f5ff
    style I fill:#fff3e0
    style R fill:#fff3e0

索引构建过程

Pagefind 的工作流程可以分为两个阶段:构建时索引运行时搜索

1. 构建时索引(Build Time)

当你运行 pagefind --source "dist" 时,Pagefind 会执行以下步骤:

flowchart TD
    Start([开始构建]) --> Scan[扫描 HTML 文件]
    Scan --> Parse[解析 HTML DOM]
    Parse --> Extract[提取内容]

    Extract --> CheckBody{检查 data-pagefind-body}
    CheckBody -->|找到| UseBody[使用标记的内容]
    CheckBody -->|未找到| UseDefault[使用 body 全部内容]

    UseBody --> Filter[应用排除规则]
    UseDefault --> Filter

    Filter --> Meta[提取元数据]
    Meta --> Tokenize[文本分词]

    Tokenize --> CheckLang{检测语言}
    CheckLang -->|英文| EnTokenizer[英文分词器]
    CheckLang -->|中文| ZhTokenizer[中文分词器 n-gram]
    CheckLang -->|其他| OtherTokenizer[对应语言分词器]

    EnTokenizer --> BuildIndex[构建倒排索引]
    ZhTokenizer --> BuildIndex
    OtherTokenizer --> BuildIndex

    BuildIndex --> CalcWeight[计算词条权重]
    CalcWeight --> Shard[索引分片 256个桶]

    Shard --> Compress[压缩处理]
    Compress --> GenFragment[生成内容片段]
    GenFragment --> WriteFiles[写入文件]

    WriteFiles --> Output[输出到 pagefind/]
    Output --> End([构建完成])

    style Start fill:#90EE90
    style End fill:#FFB6C1
    style BuildIndex fill:#FFE4B5
    style Compress fill:#E0FFFF

关键技术点:

  • 倒排索引:对于每个词条,记录它出现在哪些文档的哪些位置
  • 分片存储:将索引拆分成小块,按需加载(使用一致性哈希算法分配到 256 个桶)
  • 压缩算法:使用高效的压缩减少文件大小

索引结构详解:

pagefind/
├── pagefind.js           # 核心搜索引擎(~20KB)# - 包含哈希函数# - 索引加载器# - 搜索算法
│
├── pagefind-ui.js        # UI 组件(~15KB)
├── pagefind-ui.css       # 样式文件(~3KB)
│
├── index/                # 索引分片(256 个)
│   ├── index_00.pf       # 哈希值 0x00-0x00
│   ├── index_01.pf       # 哈希值 0x01-0x01
│   ├── ...
│   └── index_ff.pf       # 哈希值 0xFF-0xFF
│
├── fragment/             # 内容片段
│   ├── en_<hash>.pf      # 英文页面片段
│   ├── zh_<hash>.pf      # 中文页面片段
│   └── ...
│
└── filter/               # 过滤器数据(如果使用)
    ├── category.pf
    └── tags.pf

2. 运行时搜索(Runtime)

当用户输入搜索查询时的完整时序:

sequenceDiagram
    actor User as 用户
    participant UI as 搜索界面
    participant Core as Pagefind 核心
    participant Cache as 浏览器缓存
    participant Server as 静态服务器

    User->>UI: 输入 "React 教程"
    UI->>UI: 防抖延迟 (300ms)

    UI->>Core: search("React 教程")
    Core->>Core: 分词 ["React", "教程"]

    par 并行计算哈希
        Core->>Core: hash("React") = 0x42
        Core->>Core: hash("教程") = 0xA7
    end

    par 并行加载索引分片
        Core->>Cache: 检查 index_42.pf
        Cache-->>Core: 缓存未命中
        Core->>Server: GET /pagefind/index/index_42.pf
        Server-->>Core: 返回索引数据 (5KB)

        Core->>Cache: 检查 index_a7.pf
        Cache-->>Core: 缓存命中
        Cache-->>Core: 返回缓存数据
    end

    Core->>Core: 解析索引分片
    Core->>Core: 查找匹配文档<br/>"React": [1,5,23]<br/>"教程": [1,8,15]<br/>交集: [1]

    Core->>Core: 计算 TF-IDF 得分
    Core->>Core: 排序结果

    Core->>Cache: 检查 fragment_1.pf
    Cache-->>Core: 缓存未命中
    Core->>Server: GET /pagefind/fragment/zh_1.pf
    Server-->>Core: 返回内容片段 (12KB)

    Core->>Core: 提取摘要<br/>高亮关键词
    Core->>Core: 生成结果对象

    Core-->>UI: 返回搜索结果
    UI->>UI: 渲染结果列表
    UI-->>User: 显示搜索结果

    Note over Core,Server: 总耗时: ~80ms<br/>网络请求: 2 个 (17KB)<br/>缓存命中: 1 个

性能分析:

阶段 耗时 说明
用户输入 + 防抖 300ms 等待用户完成输入
分词 + 哈希计算 <5ms 纯计算,无 I/O
加载索引分片 20-50ms 取决于网络和缓存
索引查询 + 评分 5-10ms 纯内存操作
加载内容片段 15-30ms 取决于网络和缓存
摘要生成 + 渲染 5-10ms DOM 操作
总计(首次) ~80ms 不含防抖延迟
总计(缓存) ~25ms 索引和片段均已缓存

核心技术解析

1. 按需加载机制

Pagefind 最大的创新是渐进式加载。传统的客户端搜索(如 Lunr.js)需要加载完整索引:

// 传统方案:需要加载整个索引
// 假设网站有 1000 个页面,索引文件可能有 5MB
await loadFullIndex(); // 加载 5MB
search("React");

Pagefind 的方案:

// Pagefind:按需加载
search("React");
// 1. 根据 "React" 计算哈希 -> 只加载包含 "React" 的索引分片(可能只有 10KB)
// 2. 找到匹配的文档 ID
// 3. 只加载这些文档的内容片段(可能 20KB)
// 总共只需要下载 30KB,而不是 5MB

实现原理:

查询词 "React"
    ↓
计算哈希:hash("React") = 0x3A7F
    ↓
确定分片:0x3A7F % 256 = 127
    ↓
加载:GET /pagefind/index/index_127.pf
    ↓
解析分片,找到文档 ID: [5, 23, 87]
    ↓
加载内容:GET /pagefind/fragment/en_005.pf

2. 倒排索引结构

倒排索引是搜索引擎的核心数据结构:

正向索引(文档 → 词条):
文档1: ["React", "教程", "入门"]
文档2: ["Vue", "教程", "进阶"]
文档3: ["React", "进阶", "Hooks"]

倒排索引(词条 → 文档):
"React"  → [文档1, 文档3]
"Vue"    → [文档2]
"教程"   → [文档1, 文档2]
"入门"   → [文档1]
"进阶"   → [文档2, 文档3]
"Hooks"  → [文档3]

当搜索 "React 教程" 时:

  1. 查找 "React" → [文档1, 文档3]
  2. 查找 "教程" → [文档1, 文档2]
  3. 取交集 → [文档1]

3. TF-IDF 相关性评分

Pagefind 使用 TF-IDF 算法计算搜索结果的相关性:

TF(词频):词条在文档中出现的频率

TF(t, d) = 词条 t 在文档 d 中出现的次数 / 文档 d 的总词数

IDF(逆文档频率):词条的稀有程度

IDF(t) = log(总文档数 / 包含词条 t 的文档数)

TF-IDF 得分

TF-IDF(t, d) = TF(t, d) × IDF(t)

示例计算:

假设我们有 100 个文档,搜索 "React Hooks":

文档A:
- "React" 出现 10 次,文档总词数 100
  TF("React", A) = 10/100 = 0.1
  包含 "React" 的文档有 30IDF("React") = log(100/30) = 0.52
  TF-IDF("React", A) = 0.1 × 0.52 = 0.052

- "Hooks" 出现 5TF("Hooks", A) = 5/100 = 0.05
  包含 "Hooks" 的文档有 5IDF("Hooks") = log(100/5) = 1.30
  TF-IDF("Hooks", A) = 0.05 × 1.30 = 0.065

文档A 总分 = 0.052 + 0.065 = 0.117

"Hooks" 更稀有,所以权重更高。

4. 多语言分词

Pagefind 内置了多种语言的分词器:

英文分词(基于空格和标点):

"Hello, world!" → ["hello", "world"]

中文分词(基于字典和统计):

"自然语言处理" → ["自然", "语言", "处理"]
或 → ["自然语言", "处理"]
或 → ["自然语言处理"]

Pagefind 使用 n-gram 技术处理 CJK 文本:

"搜索引擎" → ["搜索", "搜索引", "搜索引擎", "索引", "索引擎", "引擎"]

这样即使查询 "搜索" 或 "引擎",也能匹配到 "搜索引擎"。

性能优化技术

Pagefind 通过多种技术实现高性能:

索引压缩(原始 10MB → 500KB,压缩率 95%):

  • 去除 HTML 标签和属性
  • 词干提取(stemming):"running" → "run"
  • 停用词过滤(去除 "the", "a", "is" 等常见词)
  • 增量编码 + Gzip 压缩

并行加载: 支持 HTTP/2 多路复用,多个词条的索引分片并行加载,总耗时 = max(单个加载时间)。

技术内幕深度剖析

1. 核心算法实现

Pagefind 是用 Rust 编写并编译为 WASM,核心逻辑包括:

哈希计算(FNV-1a 算法):

// 词条归一化(转小写、去除特殊字符)→ FNV-1a 哈希 → 映射到 0-255
hash("React") = 0x42 (66)
hash("react") = 0x42 (66)  // 大小写不敏感

索引加载器

  1. 计算词条哈希 → 确定分片编号
  2. 检查内存缓存 → 未命中则加载对应的 .pf 文件
  3. 解析二进制格式 → 存入缓存
  4. 返回词条对应的文档 ID 列表

TF-IDF 评分器

// 计算每个文档的相关性得分
score = Σ(TF × IDF × weight) × lengthNorm
// - TF: 词频
// - IDF: 逆文档频率(缓存优化)
// - weight: 自定义权重
// - lengthNorm: 长度归一化(防止长文档占优)

2. .pf 文件格式

Pagefind 使用自定义的 .pf(Pagefind Format)二进制格式:

索引文件(index_XX.pf)

  • Header:Magic Number (0x5046 'PF') + 版本 + 标志 + 条目数
  • Entries:每个词条 → 文档 ID 列表(增量编码)

示例:"React" → [1, 5, 23] 存储为 [1, +4, +18]

内容片段(fragment_XX.pf)

  • Header:Magic Number + 压缩类型 + 文档 ID + 长度
  • Metadata:JSON 格式(title, url, excerpt 等)
  • Content:原始文本 + 词条位置映射

3. 四层压缩策略

graph LR
    A[原始数据<br/>100KB] --> B[增量编码<br/>50KB]
    B --> C[VarInt 编码<br/>40KB]
    C --> D[词干提取<br/>30KB]
    D --> E[Gzip 压缩<br/>25KB]

    style E fill:#90EE90

Level 1: 增量编码(Delta Encoding)

  • 文档 ID [1, 5, 23, 45][1, +4, +18, +22]
  • 节省 50% 存储空间

Level 2: 变长整数编码(VarInt)

  • 小数字用 1 字节,大数字自动扩展
  • 1 → [0x01]128 → [0x80, 0x01]

Level 3: 词干提取(Stemming)

  • "running", "runs", "runner" → "run"
  • 减少唯一词条数量 30-40%

Level 4: Gzip 压缩

  • 文本压缩率 60-80%
  • 最终实现 95% 总压缩率

4. 三层缓存架构

graph TD
    A[搜索请求] --> B{L1 内存缓存}
    B -->|命中| C[返回结果]
    B -->|未命中| D{L2 HTTP 缓存}
    D -->|命中| C
    D -->|未命中| E{L3 Service Worker}
    E -->|命中| C
    E -->|未命中| F[网络请求]
    F --> G[更新所有缓存]
    G --> C

    style B fill:#FFE4B5
    style D fill:#E0FFFF
    style E fill:#F0E68C
缓存层级 命中延迟 容量 适用场景
L1 内存缓存 <1ms ~10MB 频繁访问的索引(LRU 淘汰)
L2 HTTP 缓存 ~5ms ~100MB 已访问的所有索引(Cache-Control)
L3 Service Worker ~10ms ~50MB 离线访问(可选)
网络请求 50-200ms - 首次访问

性能提升

  • 首次搜索:~80ms
  • 后续搜索(缓存命中):~25ms
  • 离线模式:~25ms

服务器配置(Nginx):

location /pagefind/ {
    add_header Cache-Control "public, max-age=31536000, immutable";
    gzip on;
}

性能对比

方案 初次加载 索引大小 (1000页) 搜索速度 离线支持
Pagefind ~20KB ~500KB <50ms
Algolia 0 (CDN) N/A <10ms
Lunr.js ~30KB ~3MB ~100ms

实际数据(500 页文档网站):

  • 首次搜索:下载 45KB,耗时 ~80ms
  • 后续搜索:下载 10KB,耗时 ~25ms
  • 对比 Lunr.js:减少 97% 的下载量

常见问题

Q: Pagefind 与 Algolia 如何选择?

  • Pagefind:中小型网站(< 10,000 页)、免费、离线支持、重视隐私
  • Algolia:大型网站、高级功能、极致速度、付费

Q: 支持哪些框架? 框架无关,支持 VitePress、Docusaurus、Hugo、Jekyll、Astro、Next.js(SSG)等任何生成 HTML 的工具。

Q: 是否影响 SEO? 不影响。Pagefind 的搜索 UI 是客户端渲染的,原始 HTML 内容完全不受影响。

Q: 如何更新索引? 每次构建时重新生成索引。在 CI/CD 中使用 postbuild 脚本自动化。

总结

Pagefind 为静态网站提供了轻量、高性能的搜索方案:

  • 轻量级:核心 20KB,按需加载
  • 高性能:搜索响应 < 50ms
  • 零配置:开箱即用
  • 完全静态:无需服务器,支持离线
  • 多语言:内置 CJK 分词

核心原理

  1. 倒排索引 + 分片:将索引拆分成 256 个小块
  2. 按需加载:根据查询词哈希值只加载相关分片
  3. TF-IDF 评分:计算相关性智能排序
  4. 多语言分词:支持中英文等智能分词

相关资源

3637. 三段式数组 I

作者 stormsunshine
2025年8月4日 06:34

解法一

思路和算法

数组 $\textit{nums}$ 的长度是 $n$。最直观的思路是遍历 $0 < p < q < n - 1$ 的所有下标对 $(p, q)$,判断是否同时满足三段式数组的全部条件。

  • 下标范围 $[0, p]$ 严格递增。

  • 下标范围 $[p, q]$ 严格递减。

  • 下标范围 $[q, n - 1]$ 严格递增。

当遇到满足全部条件的下标对 $(p, q)$ 时,返回 $\text{true}$。如果不存在满足全部条件的下标对 $(p, q)$,返回 $\text{false}$。

代码

###Java

class Solution {
    public boolean isTrionic(int[] nums) {
        int n = nums.length;
        for (int p = 1; p < n - 2; p++) {
            if (!isMonotonicInRange(nums, 0, p, 1)) {
                continue;
            }
            for (int q = p + 1; q < n - 1; q++) {
                if (isMonotonicInRange(nums, p, q, -1) && isMonotonicInRange(nums, q, n - 1, 1)) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean isMonotonicInRange(int[] nums, int start, int end, int direction) {
        for (int i = start; i < end; i++) {
            if ((nums[i + 1] - nums[i]) * direction <= 0) {
                return false;
            }
        }
        return true;
    }
}

###C#

public class Solution {
    public bool IsTrionic(int[] nums) {
        int n = nums.Length;
        for (int p = 1; p < n - 2; p++) {
            if (!IsMonotonicInRange(nums, 0, p, 1)) {
                continue;
            }
            for (int q = p + 1; q < n - 1; q++) {
                if (IsMonotonicInRange(nums, p, q, -1) && IsMonotonicInRange(nums, q, n - 1, 1)) {
                    return true;
                }
            }
        }
        return false;
    }

    public bool IsMonotonicInRange(int[] nums, int start, int end, int direction) {
        for (int i = start; i < end; i++) {
            if ((nums[i + 1] - nums[i]) * direction <= 0) {
                return false;
            }
        }
        return true;
    }
}

复杂度分析

  • 时间复杂度:$O(n^3)$,其中 $n$ 是数组 $\textit{nums}$ 的长度。需要遍历的下标对数量是 $O(n^2)$,每个下标对判断是否符合三段式数组的全部条件的时间是 $O(n)$,因此时间复杂度是 $O(n^3)$。

  • 空间复杂度:$O(1)$。

解法二

思路和算法

可以遍历数组 $\textit{nums}$ 一次,判断是否符合三段式的条件。

三个子数组的单调性必须依次满足严格单调递增、严格单调递减、严格单调递增。首个子数组的起始下标是 $0$,从起始下标向右遍历,遍历过程中执行如下操作。

  • 由于三个子数组都满足严格单调递增或严格单调递减,因此不允许出现相邻元素相等的情况。如果遇到相邻元素相等的情况,则一定不符合三段式的条件。

  • 如果遍历到数组 $\textit{nums}$ 的末尾或者遇到相邻元素的单调性与当前子数组的单调性条件相反的情况(已经排除相邻元素相等的情况),则可以确定当前子数组的结束下标。将当前子数组的结束下标作为下一个子数组的起始下标,继续遍历下一个子数组。

当数组 $\textit{nums}$ 可以恰好分成三个子数组且依次满足严格单调递增、严格单调递减、严格单调递增时,返回 $\text{true}$,否则返回 $\text{false}$。

代码

###Java

class Solution {
    public boolean isTrionic(int[] nums) {
        int n = nums.length;
        int index = 0;
        for (int i = 0, direction = 1; i < 3; i++, direction *= -1) {
            index = findMonotonicRangeEnd(nums, index, direction);
            if (index < 0) {
                return false;
            }
        }
        return index == n - 1;
    }

    public int findMonotonicRangeEnd(int[] nums, int start, int direction) {
        int n = nums.length;
        int end = -1;
        for (int i = start; i < n && end < 0; i++) {
            if (i < n - 1 && nums[i + 1] == nums[i]) {
                return -1;
            }
            if (i == n - 1 || (nums[i + 1] - nums[i]) * direction < 0) {
                end = i;
            }
        }
        return end > start ? end : -1;
    }
}

###C#

public class Solution {
    public bool IsTrionic(int[] nums) {
        int n = nums.Length;
        int index = 0;
        for (int i = 0, direction = 1; i < 3; i++, direction *= -1) {
            index = FindMonotonicRangeEnd(nums, index, direction);
            if (index < 0) {
                return false;
            }
        }
        return index == n - 1;
    }

    public int FindMonotonicRangeEnd(int[] nums, int start, int direction) {
        int n = nums.Length;
        int end = -1;
        for (int i = start; i < n && end < 0; i++) {
            if (i < n - 1 && nums[i + 1] == nums[i]) {
                return -1;
            }
            if (i == n - 1 || (nums[i + 1] - nums[i]) * direction < 0) {
                end = i;
            }
        }
        return end > start ? end : -1;
    }
}

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是数组 $\textit{nums}$ 的长度。最多需要遍历数组一次。

  • 空间复杂度:$O(1)$。

两种写法,推广到多个拐弯的一般情况(Python/Java/C++/Go)

作者 endlesscheng
2025年8月3日 12:22

写法一:三个循环

三段式子数组必须满足「严格递增 - 严格递减 - 严格递增」,一共三段,每一段至少要有两个数

每一段分别用一个循环寻找,具体请看 视频讲解 中的图,欢迎点赞关注~

###py

class Solution:
    def isTrionic(self, nums: List[int]) -> bool:
        n = len(nums)
        # 第一段
        i = 1
        while i < n and nums[i - 1] < nums[i]:
            i += 1
        if i == 1:  # 第一段至少要有两个数
            return False

        # 第二段
        i0 = i
        while i < n and nums[i - 1] > nums[i]:
            i += 1
        if i == i0 or i == n:  # 第二段至少要有两个数,第三段至少要有两个数
            return False

        # 第三段
        while i < n and nums[i - 1] < nums[i]:
            i += 1
        return i == n

###java

class Solution {
    public boolean isTrionic(int[] nums) {
        int n = nums.length;
        // 第一段
        int i = 1;
        while (i < n && nums[i - 1] < nums[i]) {
            i++;
        }
        if (i == 1) { // 第一段至少要有两个数
            return false;
        }

        // 第二段
        int i0 = i;
        while (i < n && nums[i - 1] > nums[i]) {
            i++;
        }
        if (i == i0 || i == n) { // 第二段至少要有两个数,第三段至少要有两个数
            return false;
        }

        // 第三段
        while (i < n && nums[i - 1] < nums[i]) {
            i++;
        }
        return i == n;
    }
}

###cpp

class Solution {
public:
    bool isTrionic(vector<int>& nums) {
        int n = nums.size();
        // 第一段
        int i = 1;
        while (i < n && nums[i - 1] < nums[i]) {
            i++;
        }
        if (i == 1) { // 第一段至少要有两个数
            return false;
        }

        // 第二段
        int i0 = i;
        while (i < n && nums[i - 1] > nums[i]) {
            i++;
        }
        if (i == i0 || i == n) { // 第二段至少要有两个数,第三段至少要有两个数
            return false;
        }

        // 第三段
        while (i < n && nums[i - 1] < nums[i]) {
            i++;
        }
        return i == n;
    }
};

###go

func isTrionic(nums []int) bool {
n := len(nums)
// 第一段
i := 1
for i < n && nums[i-1] < nums[i] {
i++
}
if i == 1 { // 第一段至少要有两个数
return false
}

// 第二段
i0 := i
for i < n && nums[i-1] > nums[i] {
i++
}
if i == i0 || i == n { // 第二段至少要有两个数,第三段至少要有两个数
return false
}

// 第三段
for i < n && nums[i-1] < nums[i] {
i++
}
return i == n
}

写法二:一个循环

如果题目改成「增减增减增」,难道要写五个循环吗?

改成统计拐弯的次数,可以推广到更一般的情况。

###py

class Solution:
    def isTrionic(self, nums: List[int]) -> bool:
        if nums[0] >= nums[1]:  # 一开始必须是递增的
            return False
        cnt = 1
        for i in range(2, len(nums)):
            if nums[i - 1] == nums[i]:
                return False
            if (nums[i - 2] < nums[i - 1]) != (nums[i - 1] < nums[i]):
                cnt += 1
        return cnt == 3  # 一定是增减增

###java

class Solution {
    public boolean isTrionic(int[] nums) {
        if (nums[0] >= nums[1]) { // 一开始必须是递增的
            return false;
        }
        int cnt = 1;
        for (int i = 2; i < nums.length; i++) {
            if (nums[i - 1] == nums[i]) {
                return false;
            }
            if ((nums[i - 2] < nums[i - 1]) != (nums[i - 1] < nums[i])) {
                cnt++;
            }
        }
        return cnt == 3; // 一定是增减增
    }
}

###cpp

class Solution {
public:
    bool isTrionic(vector<int>& nums) {
        if (nums[0] >= nums[1]) { // 一开始必须是递增的
            return false;
        }
        int cnt = 1;
        for (int i = 2; i < nums.size(); i++) {
            if (nums[i - 1] == nums[i]) {
                return false;
            }
            if ((nums[i - 2] < nums[i - 1]) != (nums[i - 1] < nums[i])) {
                cnt++;
            }
        }
        return cnt == 3; // 一定是增减增
    }
};

###go

func isTrionic(nums []int) bool {
if nums[0] >= nums[1] { // 一开始必须是递增的
return false
}
cnt := 1
for i := 2; i < len(nums); i++ {
if nums[i-1] == nums[i] {
return false
}
if (nums[i-2] < nums[i-1]) != (nums[i-1] < nums[i]) {
cnt++
}
}
return cnt == 3 // 一定是增减增
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

专题训练

见下面滑动窗口与双指针题单的「六、分组循环」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

昨天 — 2026年2月2日首页

邵氏兄弟:百年港牌,今成华人文化上市跳板

2026年2月2日 21:32

作者|宋婉心

编辑|张帆

香港娱乐业正逐步改姓“黎”。

近日香港头部电影集团绍氏兄弟发布了一条公告,表示拟收购其主要股东华人文化旗下的核心影视资产。

核心影视资产主要是,华人文化通过全资附属公司HoldCo间接控制的CMC Moon Holdings所持业务。

CMC Moon几乎涵盖了华人文化最核心的影视内容与渠道资源,包括剧集制作龙头正午阳光、电影投资制作公司上海华人影业、海外发行业务CMC Pictures,以及以UME品牌运营的影院网络和华人文化电影院线等。

公布后第二个交易日收盘,邵氏兄弟报0.32港元/股,大跌15.79%。

以资产规模计,截至2025年9月末,邵氏兄弟经审核资产总值约4.59亿元,而拟注入资产的净值高达约85.58亿元。

表面上看,这是邵氏兄弟“蛇吞象”买资产,但本质上,是华人文化反向借壳完成了上市,而邵氏中小股东,却成为这场棋局的直接买单方。

01 反向借壳

交易前,华人文化已持有邵氏29.94%股权,为第二大股东,彼时邵氏仅主营港片制作与少量发行,无线下院线布局,同时2022年至2024年营收持续萎缩。

对于华人文化而言,此前影视资产估值偏低,UME院线融资扩店也受限,缺乏港股上市平台进行资产证券化。 

而通过本次发行股份收购,华人文化持股比例跃升至59.74%,实现对邵氏的绝对控股后,华人文化得以反向借壳完成上市。

华人文化之所以不选择独立IPO,一方面是因为影视行业长期估值承压,投资者信心不足,独立IPO估值难达预期,另一方面,华人文化业务多元且分散,整合申报成本高、周期长。

通过反向借壳,华人文化既规避了港股IPO的繁琐流程,又为旗下影视资产完成证券化、为UME院线找到上市融资平台,一举解决两大痛点。交易的本质,是华人文化通过“左手倒右手”完成高效整合,兼顾效率、确定性与长期价值。

不过本次交易15.8%的折价配股,引起了二级市场的剧烈震荡,也是这场资本腾挪的争议核心。

此次配股价格0.32港元/股,较公告前收盘价折让15.8%,远超港股文娱行业并购8%-12%的平均折价率。

更关键的是,159.3亿股的发行规模占邵氏发行后总股本的91.82%,直接导致原中小股东持股比例集体腰斩,且交易未设置任何小股东补偿条款。

对此,市场解读为华人文化为“快速交割、绝对控股”的妥协——

为满足港交所公众持股不低于25%的要求,华人文化将部分股份分配给阿里、腾讯等关联方,二者合计持股达34.52%,既快速锁定控股权,又避免给二级市场抬价空间。

公告后二级市场用脚投票,邵氏股价从0.38港元跌至0.275港元,市值蒸发超三成,中小股东权益大幅缩水。

据交易公告,华人文化计划在交易完成后12个月内,打通邵氏与正午阳光、UME院线的资源,实现“内容制作、发行、线下放映”的全链路协同。

但这一规划仍面临多重考验——影视与院线的跨地域管理考验CMC的整合能力,UME院线虽布局一二线城市,但行业复苏不及预期的风险仍存;而部分邵氏小股东已发起联合问询,维权诉求或拖慢交易交割进程。

对于邵氏中小股东而言,短期的权益缩水已成定局,能否通过长期估值修复回本,取决于华人文化的整合效率与资产兑现能力。

而对于整个文娱行业,这场交易或许标志着港股将成为内地文娱资产证券化的重要阵地。

02 香港娱乐的幕后一哥

香港娱乐产业的衰落有目共睹。

2018年开始,TVB已经连续亏损7年,2024年全年财报公布时,公司表示预计2025年将实现盈利。与此同时,内地同样传统电视出身的芒果超媒已经增长至500亿元市值左右,是TVB母公司市值的30多倍。

作为内地的“默多克”,黎瑞刚执掌的华人文化在传媒娱乐领域的布局几乎囊括了所有领域,不限于影视、音乐、体育等板块,而在香港,华人文化同样早早介入。

华人文化自2015年起主导对TVB的战略投资,并于2020年成为TVB最大股东。2023年及之后多次连任董事,实际掌控TVB战略方向。

可以看到的是,在投资的初始几年,黎瑞刚一直对TVB按兵不动,直到近年TVB近乎濒危,才进行了一系列调整。比如开启直播带货,和芒果TV、优酷等内地平台打造《声生不息·港乐季》《无限超越班》等。

以《声生不息·港乐季》为例,TVB只负责提供艺人、版权等资源,湖南卫视则是投资方兼制作方。据真故研究室报道,该节目在香港播出后,所有收入归TVB所有。重返TVB的曾志伟直言:拿了个不要钱的节目回来。

但即使如此,一系列改革对TVB实际的业绩翻身依旧收效甚微。

在这档综艺播出、开启直播带货的2022年,TVB虽然整体营收增长了24%,但亏损仍在扩大,相比前年的3.14亿港元亏损还增加了2400万港元,且二级市场也颇为冷淡。

直播带货曾在2023年大幅带动股价创四年新高,但也叫好不叫座。当年TVB电子商贸业务收入大幅缩水,到了2024年,电商业务收入仅剩1.26亿港元,也成了TVB目前唯一亏损的业务。

TVB股价在2023年触及18港元高点后,至今已一路跌回至3港元左右。

(TVB近三年股价)

而这次华人文化反向借的“壳”绍氏兄弟影业,和TVB一样,也是邵逸夫手下“时代的眼泪”。

“绍氏武侠片”曾是香港上个世纪50年代至80年代期间的香港电影招牌。上世纪60年代,武侠文化兴起,而邵逸夫遵循商业制片路线,坚持“观众至上”,基于对当时市民观众兴趣的洞察,绍氏影业成功抓住了武侠片的观看热潮。

此外在公司经营上,邵逸夫引入的仿美式制片厂制度,也让电影制作更为标准化和工业化,被业内称为“东方好莱坞”,进而给绍氏兄弟带来了丰厚利润。

英国《每日电讯报》称,巅峰时期,每周都有超过200万观众观看邵氏的电影。

但要注意的是,这个活跃在上个世纪的老牌香港电影制片厂,和如今的“绍氏兄弟”实际上是两家不同的实体——绍氏兄弟的核心内容资产早已在2000年就永久出售给了马来西亚的天映娱乐。

此后,绍氏兄弟就已经只剩下一个品牌空壳,约760部经典电影版权和和绍氏再无关系,绍氏兄弟也正式进入衰退期。直至2009年,绍氏兄弟完成私有化退市。

接下来的故事便迎来华人文化登场。2015年,华人文化密集出手投资、组建文娱帝国之时,控股了一家名为美克国际的香港上市公司,与此同时,作为TVB的第一大股东,华人文化凭借TVB与绍氏兄弟的渊源,取得了后者的品牌使用权。

随后在2016年,华人文化将美克国际更名为了“绍氏兄弟”,一个本质上与人们认知中的老牌制片厂毫无关系的新“绍氏兄弟”。

而如今,这一“绍氏兄弟”的壳再次被华人文化拿来作为上市平台进行资本化操作,注入了内地的影视厂牌和院线资产。

至此,新“绍氏兄弟”实体内,融合了香港制片厂品牌、港股上市平台、内地影视内容和院线以及内地资本。华人文化俨然已经成为香港和内地影视圈融合的主要试验场。

只是从过去几年的实践来看,在沉疴旧疾未解决的背景下,香港娱乐嫁接内地资源迎来短暂的回春后,终又回归平静。

*免责声明:

本文内容仅代表作者看法。

市场有风险,投资需谨慎。在任何情况下,本文中的信息或所表述的意见均不构成对任何人的投资建议。在决定投资前,如有需要,投资者务必向专业人士咨询并谨慎决策。我们无意为交易各方提供承销服务或任何需持有特定资质或牌照方可从事的服务。

关注获取更多资讯

阿里系高管加盟擎天租

2026年2月2日 20:56
2月2日,全球机器人租赁服务平台擎天租宣布完成新一轮关键管理层布局。阿里资深渠道建设专家李立恒(花名:黑猫警长),与阿里商业战略与组织变革专家王明峰(花名:天相)正式加入,分别出任擎天租联席总裁与首席战略官(CSO)。此外,擎天租也正式宣布启动全国城市合伙人战略计划。目前,公司管理层除新加入的两位阿里系高管之外,智元合伙人、联席总裁姜青松出任公司董事长;飞阔科技创始人李一言担任CEO;前饿了么副总裁陈艳霞出任COO;喵掌柜创始人李可为担任CMO。(界面)

抖音与2026年总台春晚达成合作

2026年2月2日 20:55
36氪获悉,抖音与中央广播电视总台《2026年春节联欢晚会》联合宣布,抖音成为2026年总台新媒体《竖屏看春晚》独家合作伙伴。与此同时,抖音还将提供横屏、无障碍、幕后花絮等多路直播,并依托多机位并行直播技术,在单个直播间同时呈现多路直播画面。

SwiftUI快速入门指南-Viewbuilder篇

作者 xiAo_Ju
2026年2月2日 20:44

背景

本文帮助有Swift基础的同学,快速入门SwiftUI,基于cursour整理

主要分为四个部分:

@ViewBuilder

@ViewBuilder 是一个函数构建器(result builder),让你可以在闭包里写多行视图语句,最终合成为一个 View

没有它,你只能返回一个视图;有了它,你可以:

@ViewBuilder
var body: some View {
    Text("A")
    Text("B")
}

它解决了什么问题

  • 简化条件分支:允许 if/else 直接写在 body 里
  • 组合多个子视图:不用手动包一层 VStack 或 Group
  • 让 DSL 更像声明式 UI

它是怎么工作的(简化理解)

@ViewBuilder 会把闭包里的多行语句编译成:

  • 1 个视图 → 直接返回
  • 2~10 个视图 → 组合成 TupleView
  • 超过 10 个 → 需要你自己包一层容器(比如 VStack)

常见用法

1) 在 body 中

var body: some View {
    if isLogin {
        Text("已登录")
    } else {
        Text("未登录")
    }
}

2) 自定义组件接收视图闭包

struct Card<Content: View>: View {
    let content: Content
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    var body: some View { content }
}

常见坑

  • 不是所有地方都有 @ViewBuilder:比如 Button 的 action 闭包就不是,它只接受一段逻辑代码
  • 返回类型必须一致:if/else 的两边要返回同一种 View(SwiftUI 会帮你通过 AnyView 或 Group 解决,但要理解这个限制)
  • 性能影响:AnyView 会抹掉类型信息,尽量少用

@ViewBuilder使用时机

应该用 @ViewBuilder
  • 需要返回多视图:一个 API 想让调用方传入多行视图(比如自定义容器/组件)
  • 需要在闭包里写 if/else 或 switch:让调用方写条件视图更自然
  • 自定义 DSL:你在做类似 HStack { ... } 这种声明式结构

典型场景

  • 组件初始化器参数接收视图闭包:
  init(@ViewBuilder content: () -> Content) { ... }
  • 组件内部某个函数返回 View
@ViewBuilder
  func header() -> some View { ... }
不该用 @ViewBuilder
  • 闭包不是返回 View 的:比如 Button(action:)、onTapGesture 这种逻辑闭包
  • 只需要一个 View:没必要增加编译器负担
  • 会导致类型被抹掉:如果你因此被迫用 AnyView,要谨慎
  • 性能敏感的热点视图:大量分支+AnyView 会增加 diff 开销

典型场景

  • 事件处理闭包(逻辑,不是视图)
  • 简单组件:var body: some View { Text("Hello") } 不需要额外标注

快速判断口诀

 “闭包里有多行视图或条件视图,就用;纯逻辑或单一视图,就不用。

变幻的光影,不变的干草堆 - 肘子的 Swift 周报 #121

作者 Fatbobman
2026年2月2日 22:00

两周前,借着参加 iOS Conf SG 2026 的契机,我造访了新加坡国立美术馆,并有幸参观了《走进现代:波士顿美术博物馆印象派大师展》。尽管此前也看过不少优秀的展览,但这次经历仍带来了某种不同寻常的触动。

ST泉为:被债权人申请重整及预重整

2026年2月2日 20:31
36氪获悉,ST泉为公告,公司收到债权人青岛泰上青阳供应链管理有限公司(简称“申请人、债权人”)发来的通知,获悉申请人以公司不能清偿到期债务且明显缺乏清偿能力,但具有重整价值为由,于1月29日向东莞市中级人民法院申请对公司进行重整,并申请启动预重整程序。公司尚未收到法院对申请人申请公司重整及预重整的受理文件,申请人的重整及预重整申请能否被法院裁定受理存在不确定性。

跨平台框架怎么选:16 个框架全景对比(2026 版)

作者 jump_jump
2026年2月2日 20:29

跨平台不是"能不能跑",而是"用哪条技术路线换哪种确定性"。

选错框架的代价:某团队用 Electron 做笔记应用,上线后用户反馈"启动 5 秒,内存 500MB"。重构用了 3 个月。如果一开始选 Tauri 或 Wails,这个坑完全可以避免。

本文目标:帮你在动手前想清楚。

覆盖范围:16 个框架,4 大技术路线

  • 主流稳定:Flutter、React Native、Electron、Qt(生产环境)
  • 新兴可靠:Wails(Go)、Dioxus(Rust)、Tauri(已值得试水)
  • 垂直场景:Slint(嵌入式)、Uno(C# WASM)、NativeScript(Vue/Angular)
  • 探索阶段:Lynx、Valdi、Electrobun、GPUI

阅读建议

  • 想快速决策?→ 直接看"快速决策表"
  • 想深度了解?→ 按章节完整阅读
  • 想对比细节?→ 查看"指标矩阵"

第一章:先搞懂底层逻辑

在看具体框架前,你需要理解一个核心问题:UI 是怎么画到屏幕上的?

不同的"画法"决定了框架的基因,也决定了它擅长什么、不擅长什么。

1.1 四种技术路线

路线一:自绘渲染(Self-rendering)

原理:框架自己实现一套渲染引擎,拿到系统给的"画布"(Canvas/Surface),一笔一画把 UI 画出来。

类比:你买了一块空白画布,用自己的颜料和画笔画画。画出来的风格完全由你决定,跟画布是什么牌子的没关系。

代表框架:Flutter、Lynx、Qt Quick、GPUI、Dioxus、Slint

优势

  • 跨端一致性极强——因为渲染逻辑是自己写的,不依赖系统控件
  • 动效表现好——可以做到 60fps 甚至 120fps 的流畅动画
  • 可控性高——想改渲染管线?自己动手就行

劣势

  • 包体更大——要打包渲染引擎
  • 与系统"格格不入"——比如 iOS 的橡皮筋效果、Android 的 Material You 动态取色,需要额外适配
  • 无障碍支持需要额外工作
┌─────────────────────────────────────┐
│           你的应用代码               │
├─────────────────────────────────────┤
│         框架的渲染引擎               │  ← 这一层是框架自己实现的
├─────────────────────────────────────┤
│    系统图形 API (Metal/Vulkan/GL)   │
├─────────────────────────────────────┤
│              GPU                     │
└─────────────────────────────────────┘

路线二:原生控件映射(Native Bridging)

原理:框架把你写的代码"翻译"成原生控件调用。你写 <Button>,框架帮你调用 iOS 的 UIButton 或 Android 的 MaterialButton

类比:你是导演,给演员(原生控件)下指令。演员按照各自平台的"表演风格"来演,iOS 演员演得像 iOS,Android 演员演得像 Android。

代表框架:React Native、.NET MAUI、Uno Platform、NativeScript、Valdi

优势

  • 原生体验——因为用的就是原生控件
  • 系统功能集成方便——推送、权限、传感器等直接调用
  • 无障碍支持天然继承

劣势

  • 跨端一致性差——同一份代码在不同平台上长得不一样
  • 有"桥接"成本——JS 和原生通信需要序列化/反序列化
  • 复杂动效难做——要协调多个原生控件
┌─────────────────────────────────────┐
│           你的应用代码               │
├─────────────────────────────────────┤
│       框架的桥接层 (Bridge)          │  ← 翻译 + 通信
├─────────────────────────────────────┤
│   原生控件 (UIKit / Android Views)  │
├─────────────────────────────────────┤
│              系统                    │
└─────────────────────────────────────┘

路线三:WebView/Chromium 方案

原理:用 Web 技术栈(HTML/CSS/JS)写 UI,通过 WebView 或内嵌 Chromium 来渲染。

类比:在应用里开了一个"浏览器窗口",你的 UI 实际上是一个网页。

代表框架:Electron、Tauri、Wails、Electrobun

优势

  • 前端团队无缝上手——就是写网页
  • 生态巨大——npm 上百万个包随便用
  • 开发效率高——热更新、DevTools 一应俱全

劣势

  • 资源占用——Chromium 本身就吃内存
  • 启动慢——要初始化整个浏览器引擎
  • "不够原生"——滚动、右键菜单等细节需要额外打磨
┌─────────────────────────────────────┐
│      你的 Web 应用 (HTML/CSS/JS)    │
├─────────────────────────────────────┤
│   WebView / Chromium / 系统浏览器    │
├────────────────┬────────────────────┤
│  后端进程      │   系统 API 调用    │
│  (Node/Rust)   │                    │
└────────────────┴────────────────────┘

Electron vs Tauri vs Electrobun 核心对比

维度 Electron Tauri Electrobun
渲染引擎 内嵌 Chromium 系统 WebView 系统 WebView/CEF
后端语言 Node.js Rust Bun (TypeScript)
包体大小 150MB+ 3-10MB 10-30MB
启动速度 慢(初始化大) 中等
适合团队 前端团队 愿意学 Rust 前端团队

路线四:逻辑共享优先(Shared Logic First)

原理:只共享业务逻辑和数据层,UI 各平台自己写(或用 Compose Multiplatform 部分共享)。

类比:后厨(业务逻辑)是统一的,但前台装修(UI)各店不同。

代表框架:Kotlin Multiplatform (KMP)

优势

  • 原生体验最佳——UI 就是原生写的
  • 渐进式迁移——可以一点点把逻辑抽到共享层
  • 风险可控——UI 出问题不影响共享逻辑

劣势

  • UI 要写多份(除非用 Compose Multiplatform)
  • 团队需要掌握多平台 UI 开发
  • 共享层的边界需要仔细设计
┌──────────────────────────────────────────────────┐
│                  共享层 (Kotlin)                  │
│         网络、数据库、业务逻辑、状态管理           │
├─────────────────┬────────────────┬───────────────┤
│   Android UI    │    iOS UI      │   Desktop UI  │
│   (Compose)     │   (SwiftUI)    │  (Compose)    │
└─────────────────┴────────────────┴───────────────┘

1.2 一张图看懂路线选择

                        你的核心诉求是什么?
                              │
            ┌─────────────────┼─────────────────┐
            ▼                 ▼                 ▼
       跨端一致性          原生体验           开发效率
       视觉完全统一        系统深度集成        快速上线
            │                 │                 │
            ▼                 ▼                 ▼
       自绘渲染           原生映射          WebView 方案
    Flutter/Lynx/      RN/MAUI/Uno/      Electron/Tauri/
    Dioxus/Slint/Qt    NativeScript/KMP  Wails/Electrobun

技术栈快速匹配

你的团队主要用什么语言?
│
├─ JavaScript/TypeScript
│  ├─ React → React Native / Lynx
│  ├─ Vue/Angular → NativeScript
│  └─ 任意框架 → Electron / Tauri / Wails / Electrobun
│
├─ Dart → Flutter
│
├─ C# → .NET MAUI / Uno Platform(需要 WASM)
│
├─ C++ → Qt / Slint(嵌入式)
│
├─ Go → Wails(桌面)
│
├─ Kotlin → KMP
│
└─ Rust
   ├─ Web 前端 → Tauri
   ├─ 全栈(含 UI)→ Dioxus
   ├─ 嵌入式 → Slint
   └─ 极致性能 → GPUI

第二章:16 个框架逐一拆解

下面我们按"成熟度从高到低"的顺序介绍每个框架。为了便于理解,我们将框架按技术路线分组呈现。

2.1 Flutter(Google,2018 稳定版)

一句话定位:自绘渲染的"全能选手",跨端一致性最强的主流方案。

技术栈

  • 语言:Dart(Google 自研,语法类似 Java/JS 混合体)
  • 渲染:Skia 引擎 → 正在迁移到 Impeller(iOS 已默认启用)
  • 架构:Widget 树 + 声明式 UI

适合场景

  • 品牌型应用,强调视觉一致性(如 Google Pay、阿里闲鱼)
  • 重动效、重交互的应用(如游戏化电商、社交)
  • 需要同时覆盖移动 + Web + 桌面

不太适合

  • 需要深度系统集成的工具类应用(如文件管理器)
  • 团队对 Dart 抵触强烈
  • 包体大小极度敏感(Flutter 最小包体约 4-5MB)

真实案例

  • Google Pay:全球支付应用,Flutter 重写后开发效率提升 70%
  • 闲鱼:阿里的二手交易平台,首页用 Flutter 实现
  • BMW:车载信息娱乐系统

代码示例(感受一下 Dart 风格):

// 一个简单的计数器页面
class CounterPage extends StatefulWidget {
  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('计数器')),
      body: Center(
        child: Text('点击了 $_count 次', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() => _count++),
        child: Icon(Icons.add),
      ),
    );
  }
}

入门步骤

  1. 安装 Flutter SDK:docs.flutter.dev/get-started…
  2. 配置平台工具链(Android Studio + SDK;macOS 需 Xcode)
  3. 运行环境检查:flutter doctor
  4. 创建项目:flutter create my_app
  5. 运行:cd my_app && flutter run

常见坑

  • 热重载失效:有时需要热重启(Shift+R)或完全重启
  • 包体优化:使用 --split-debug-info--obfuscate 可减小约 30%
  • iOS 审核:确保 Info.plist 里的权限说明清晰

2.2 React Native(Meta,2015)

一句话定位:用 React 写原生应用,前端团队的"舒适区扩展"。

技术栈

  • 语言:JavaScript/TypeScript + React
  • 渲染:映射到原生控件
  • 架构:新架构(Fabric + TurboModules)正在推进

适合场景

  • 团队是 React 技术栈,想复用前端能力
  • 需要原生体验,但开发效率也很重要
  • 应用以内容展示为主(如新闻、电商列表页)

不太适合

  • 复杂动效(如游戏、3D 展示)
  • 需要跨端 UI 完全一致
  • 对启动速度要求极高(RN 的 JS 引擎初始化需要时间)

真实案例

  • Facebook/Instagram:部分页面使用 RN
  • Shopify:商家管理应用
  • Discord:移动端部分功能

代码示例

// React Native 的代码对 React 开发者很熟悉
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <View style={styles.container}>
      <Text style={styles.text}>点击了 {count} 次</Text>
      <TouchableOpacity style={styles.button} onPress={() => setCount(c => c + 1)}>
        <Text style={styles.buttonText}>+1</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  text: { fontSize: 24, marginBottom: 20 },
  button: { backgroundColor: '#007AFF', padding: 15, borderRadius: 8 },
  buttonText: { color: 'white', fontSize: 18 },
});

入门步骤

  1. 安装 Node.js(推荐 18+)
  2. 选择初始化方式:
    • 快速上手:npx create-expo-app my-app(Expo 托管方案)
    • 完全控制:npx react-native init MyApp(裸 RN)
  3. 配置原生工具链(Android Studio + Xcode)
  4. 运行:npx expo startnpx react-native run-ios

常见坑

  • 桥接性能:大量数据传递时考虑用新架构的 JSI
  • 第三方库兼容性:检查是否支持新架构
  • 启动时间:用 Hermes 引擎替代 JSC 可提升 30-50%

2.3 NativeScript(Progress Software,2014)

一句话定位:用 Vue/Angular/Vanilla JS 写原生应用,填补非 React 前端技术栈的空白。

技术栈

  • 语言:JavaScript/TypeScript + Vue/Angular/Vanilla JS
  • 渲染:映射到原生控件(与 RN 类似)
  • 架构:直接访问原生 API(无桥接层)

适合场景

  • Vue 或 Angular 技术栈的团队
  • 需要直接访问原生 API
  • 想要原生体验的移动应用

不太适合

  • React 技术栈(直接用 React Native)
  • 需要复杂动效
  • 桌面端需求(主要支持移动端)

真实案例

  • SAP:企业应用
  • Strudel:音乐流媒体应用

代码示例(Vue 风格):

<template>
  <Page>
    <ActionBar title="计数器"/>
    <StackLayout class="p-20">
      <Label :text="`点击了 ${count} 次`" class="text-center text-2xl mb-4"/>
      <Button text="+1" @tap="count++" class="btn btn-primary"/>
    </StackLayout>
  </Page>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>

入门步骤

  1. 安装 Node.js 和 NativeScript CLI:npm install -g @nativescript/cli
  2. 创建项目:ns create my-app --vue--angular
  3. 配置原生工具链(Android Studio + Xcode)
  4. 运行:ns run iosns run android

与 React Native 的对比

维度 React Native NativeScript
框架支持 React Vue/Angular/Vanilla
原生访问 通过桥接 直接访问
性能 有桥接开销 理论上更快
生态 更大 较小

2.4 Electron(GitHub/OpenJS Foundation,2013)

一句话定位:Web 技术栈做桌面应用的"事实标准",简单粗暴但有效。

技术栈

  • 前端:HTML/CSS/JS(任意前端框架)
  • 后端:Node.js(完整的 Node API)
  • 渲染:Chromium

适合场景

  • 快速把 Web 应用搬到桌面
  • 团队只有前端能力
  • 对包体大小和内存占用不敏感

不太适合

  • 资源敏感型应用(如系统工具)
  • 需要极致启动速度
  • 用户设备配置较低

真实案例

  • VS Code:微软的代码编辑器(证明 Electron 可以做出高性能应用)
  • Slack:团队协作工具
  • Discord:桌面端
  • Figma:桌面端

资源占用参考

  • 空项目包体:~150MB(压缩后)
  • 空项目内存:~80-150MB
  • VS Code 内存:~300-800MB(取决于打开的文件和扩展)

入门步骤

  1. 初始化项目:npm init -y
  2. 安装 Electron:npm install -D electron
  3. 创建 main.js
const { app, BrowserWindow } = require('electron');

app.whenReady().then(() => {
  const win = new BrowserWindow({ width: 800, height: 600 });
  win.loadFile('index.html');
});
  1. 添加启动脚本到 package.json"start": "electron ."
  2. 运行:npm start

性能优化技巧

  • 使用 BrowserWindowshow: false + ready-to-show 事件避免白屏
  • 延迟加载非必要模块
  • 考虑使用 contextIsolation 提升安全性

2.5 Qt / Qt Quick(The Qt Company,1995/2010)

一句话定位:工业级跨平台方案,嵌入式和桌面的"老大哥"。

技术栈

  • 语言:C++(核心)+ QML(声明式 UI)
  • 渲染:RHI(Rendering Hardware Interface),支持 Vulkan/Metal/D3D/OpenGL
  • 架构:信号槽机制 + 属性绑定

适合场景

  • 工业软件、医疗设备、汽车 HMI
  • 嵌入式系统(Linux 嵌入式、MCU)
  • 对性能和稳定性要求极高

不太适合

  • 快速原型验证(学习曲线陡)
  • 小团队短周期项目
  • 纯移动端应用(移动端生态弱于 Flutter/RN)

真实案例

  • 特斯拉 Model S/X:早期车载系统
  • 达芬奇手术机器人:控制界面
  • Autodesk Maya:部分 UI
  • VirtualBox:虚拟机管理界面

代码示例(QML):

// QML 声明式 UI,类似 JSON 但带逻辑
import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
    width: 400
    height: 300
    visible: true
    title: "计数器"

    Column {
        anchors.centerIn: parent
        spacing: 20

        Text {
            text: "点击了 " + counter + " 次"
            font.pixelSize: 24
        }

        Button {
            text: "+1"
            onClicked: counter++
        }
    }

    property int counter: 0
}

许可证说明

  • 开源版(LGPL/GPL):可免费商用,但有一些限制(如动态链接、开源要求)
  • 商业版:按开发者人数收费,约 $300-500/月/人

入门步骤

  1. 下载 Qt Online Installer:www.qt.io/download
  2. 安装 Qt 6.x + Qt Creator
  3. 创建新项目 → Qt Quick Application
  4. 选择目标 Kit(Desktop/Android/iOS)
  5. 运行(Qt Creator 一键构建)

2.6 .NET MAUI(Microsoft,2022)

一句话定位:C# 团队的跨平台方案,微软生态的"官方答案"。

技术栈

  • 语言:C#
  • UI:XAML 或 C# Markup
  • 渲染:原生控件映射(类似 RN)

适合场景

  • 企业内部应用(与 Azure、Office 365 集成好)
  • 已有 C#/.NET 技术栈的团队
  • Windows 优先,兼顾其他平台

不太适合

  • 需要极致跨端一致性
  • 非 .NET 团队(学习成本高)
  • iOS/Android 优先的消费级应用

代码示例

// .NET MAUI 的 XAML + C# 模式
// MainPage.xaml
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui">
    <VerticalStackLayout Spacing="20" VerticalOptions="Center">
        <Label x:Name="CounterLabel" Text="点击了 0 次" FontSize="24" HorizontalOptions="Center"/>
        <Button Text="+1" Clicked="OnCounterClicked" HorizontalOptions="Center"/>
    </VerticalStackLayout>
</ContentPage>

// MainPage.xaml.cs
public partial class MainPage : ContentPage
{
    int count = 0;

    public MainPage() => InitializeComponent();

    void OnCounterClicked(object sender, EventArgs e)
    {
        count++;
        CounterLabel.Text = $"点击了 {count} 次";
    }
}

入门步骤

  1. 安装 .NET 8 SDK:dotnet.microsoft.com/download
  2. 安装 MAUI 工作负载:dotnet workload install maui
  3. 创建项目:dotnet new maui -n MyApp
  4. 用 Visual Studio 或 VS Code 打开
  5. 选择目标平台运行

2.7 Uno Platform(Uno Platform,2018)

一句话定位:C# 生态的"全平台方案",比 .NET MAUI 更早、支持 WebAssembly。

技术栈

  • 语言:C#
  • UI:XAML(与 UWP/WinUI 兼容)
  • 渲染:各平台原生控件 + WebAssembly 支持
  • 架构:基于 WinUI API surface

与 .NET MAUI 的关键区别

维度 .NET MAUI Uno Platform
发布时间 2022 2018
WebAssembly 不支持 支持(核心优势)
API 来源 Xamarin.Forms 演进 WinUI/UWP
Windows 优先度 中等 高(WinUI 语法)
Linux 支持 有限 通过 Skia 支持

适合场景

  • 需要 WebAssembly 支持(在浏览器中运行)
  • 熟悉 WinUI/UWP 的团队
  • 需要更广泛的平台支持(包括 Linux、Tizen)
  • Windows 应用需要迁移到其他平台

不太适合

  • 新项目且对 WebAssembly 无需求(考虑 MAUI)
  • 不熟悉 XAML 的团队
  • 需要最轻量级的移动应用

真实案例

  • HSBC:银行应用的部分功能
  • Bluebeam:建筑协作软件

代码示例

<!-- MainPage.xaml - 与 WinUI 语法兼容 -->
<Page x:Class="MyApp.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <StackPanel Spacing="20" HorizontalAlignment="Center" VerticalAlignment="Center">
        <TextBlock x:Name="CounterText" Text="点击了 0 次" FontSize="24"/>
        <Button Content="+1" Click="OnCounterClicked"/>
    </StackPanel>
</Page>
// MainPage.xaml.cs
public sealed partial class MainPage : Page
{
    private int _count = 0;

    public MainPage()
    {
        this.InitializeComponent();
    }

    private void OnCounterClicked(object sender, RoutedEventArgs e)
    {
        _count++;
        CounterText.Text = $"点击了 {_count} 次";
    }
}

入门步骤

  1. 安装 .NET SDK 和 Uno Platform 模板:
    dotnet new install Uno.Templates
    
  2. 创建项目:
    dotnet new unoapp -o MyApp
    
  3. 选择目标平台(iOS/Android/WebAssembly/Windows/macOS/Linux)
  4. 运行:
    • WebAssembly: dotnet run --project MyApp.Wasm
    • 移动端:用 Visual Studio 或 Rider

WebAssembly 优势示例

# 构建 WebAssembly 版本
dotnet publish MyApp.Wasm -c Release

# 直接部署到 Web 服务器,无需应用商店审核
# 用户通过浏览器访问即可使用

2.8 Tauri(Tauri Programme,2022 v1.0)

一句话定位:Electron 的"轻量替代品",用系统 WebView + Rust 后端。

技术栈

  • 前端:任意 Web 框架(React/Vue/Svelte/原生)
  • 后端:Rust
  • 渲染:系统 WebView(macOS: WKWebView, Windows: WebView2, Linux: WebKitGTK)

与 Electron 的关键区别

维度 Electron Tauri
包体(空项目) ~150MB ~3MB
内存(空项目) ~100MB ~30MB
后端语言 Node.js Rust
WebView 内嵌 Chromium 系统自带
跨端一致性 高(同一个 Chromium) 中(系统 WebView 版本不同)

适合场景

  • 在意包体大小和资源占用
  • 团队愿意学 Rust(或只做简单后端逻辑)
  • 不需要复杂的 Node.js 生态

不太适合

  • 需要保证不同系统上渲染完全一致
  • 后端逻辑复杂且团队不熟悉 Rust
  • 需要使用大量 Node.js 包

入门步骤

  1. 安装 Rust:rustup.rs/
  2. 安装系统依赖(Linux 需要 WebKitGTK)
  3. 创建项目:npm create tauri-app@latest
  4. 选择前端模板(React/Vue/Svelte/Vanilla)
  5. 开发:npm run tauri dev
  6. 构建:npm run tauri build

Rust 后端示例

// src-tauri/src/main.rs
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
// 前端调用
import { invoke } from '@tauri-apps/api/tauri';
const greeting = await invoke('greet', { name: 'World' });

2.9 Wails(Wails Project,2019 / v2 2022)【重点推荐】

一句话定位:Go + WebView 的桌面应用方案,填补 Go 技术栈空白,比 Tauri 学习曲线更低。

技术栈

  • 前端:任意 Web 框架(React/Vue/Svelte/原生)
  • 后端:Go
  • 渲染:系统 WebView(与 Tauri 相同)
  • 绑定:Go 方法直接暴露给前端

核心优势

优势 说明 对比
学习曲线低 Go 比 Rust 容易学 比 Tauri 门槛低 50%
类型安全 自动生成 TS 类型 编译时发现错误
并发能力强 goroutine 原生支持 适合高并发场景
包体适中 10-15MB 比 Electron 小 90%
编译快 Go 编译速度快 比 Rust 快 5-10 倍

桌面 WebView 方案全面对比

维度 Electron Tauri Wails Electrobun
后端语言 Node.js Rust Go Bun (TS)
学习曲线 低(JS/TS) 高(Rust陡) 低(Go易学) 低(TS)
包体大小 150MB 3MB 10MB 15MB
内存占用 100MB 30MB 45MB 50MB
编译速度 无需编译 慢(Rust) 快(Go)
并发模型 事件循环 异步+线程 goroutine 异步
类型安全 JS→TS 手动定义 自动生成TS TS 原生
生态成熟度 5/5 4/5 3/5 2/5

适合场景

  • Go 技术栈团队做桌面应用(这是最主要的使用场景)
  • 后端逻辑复杂,需要高并发处理(如数据同步、文件处理)
  • 需要调用 Go 生态的库(如 gRPC、各种数据库驱动)
  • 在意包体大小,但不想学 Rust
  • 系统工具类应用(文件管理、网络工具、开发工具)

不太适合

  • 需要跨平台 UI 完全一致(WebView 版本不同)
  • 需要移动端支持(Wails 主要是桌面)
  • 复杂的前端逻辑但后端很简单(考虑 Electron)
  • 团队完全是前端,没人会 Go

真实案例

  • LocalSend:跨平台文件传输工具(开源,6k+ stars)
  • Clash Verge:代理工具的 GUI 版本
  • 多个企业内部工具(数据分析、运维面板)

代码示例(完整的类型安全流程):

步骤 1:后端 Go 方法

// app.go - 定义后端方法
type App struct {
    ctx context.Context
}

func (a *App) Greet(name string) string {
    return fmt.Sprintf("Hello %s!", name)
}

func (a *App) ProcessFile(path string) error {
    // 利用 Go 的 goroutine 并发处理
    go func() {
        // 后台处理文件
    }()
    return nil
}

步骤 2:Wails 自动生成 TypeScript 类型

// wailsjs/go/models.ts - 自动生成,无需手写
export namespace main {
    export class App {
        static Greet(name: string): Promise<string>;
        static ProcessFile(path: string): Promise<void>;
    }
}

步骤 3:前端调用(完全类型安全)

import { Greet } from '../wailsjs/go/main/App';

const result = await Greet("World");  // ✅ 类型正确
// await Greet(123);  // ❌ TypeScript 编译错误

核心优势:前后端接口不匹配在编译时就能发现,而不是运行时报错。

快速开始(5 分钟)

# 1. 安装 CLI
go install github.com/wailsapp/wails/v2/cmd/wails@latest

# 2. 检查环境
wails doctor

# 3. 创建项目(选择模板:react/vue/svelte)
wails init -n myapp -t react

# 4. 开发(热重载)
cd myapp && wails dev

# 5. 构建
wails build  # 输出: myapp.app / myapp.exe / myapp

Wails v2 vs v3(2025 重大更新)

Wails v3 正在开发中,主要改进:

  • 原生移动端支持(iOS/Android)
  • 插件系统(类似 Tauri 的插件)
  • 更好的 TypeScript 集成
  • 自动更新支持

性能优化技巧

  1. 使用 Go 的并发优势
// 并行处理多个任务
func (a *App) ProcessMultipleFiles(files []string) {
    var wg sync.WaitGroup
    for _, file := range files {
        wg.Add(1)
        go func(f string) {
            defer wg.Done()
            // 处理文件
        }(file)
    }
    wg.Wait()
}
  1. 使用事件系统(前后端通信):
// 后端发送事件
runtime.EventsEmit(a.ctx, "progress", Progress{
    Current: 50,
    Total: 100,
})
// 前端监听事件
import { EventsOn } from '../wailsjs/runtime';

EventsOn('progress', (data) => {
    console.log(`Progress: ${data.current}/${data.total}`);
});
  1. 按需构建(减小包体):
# 只构建当前平台
wails build

# 跨平台构建
wails build -platform darwin/amd64,darwin/arm64,windows/amd64

常见问题

问题 解决方案
Windows 缺少 WebView2 引导用户安装 WebView2 Runtime
跨平台 WebView 差异 测试各平台,使用 polyfill
Go 依赖管理 运行 go mod tidy
前端资源路径错误 检查 wails.json 配置

Wails vs Tauri 选择指南

维度 选 Wails 选 Tauri
团队技能 熟悉 Go / 不想学 Rust 愿意学 Rust
后端需求 高并发(goroutine) 一般
包体要求 10MB 可接受 要求最小(3MB)
编译速度 要求快 可接受慢
类型安全 要自动生成 手动定义可接受
生态成熟度 可接受成长期 要求更成熟

2.10 Kotlin Multiplatform / KMP(JetBrains,2023 稳定版)

一句话定位:Android 团队扩展 iOS 的"最小阻力路径",逻辑共享优先。

技术栈

  • 语言:Kotlin
  • 共享层:commonMain(纯 Kotlin,编译到各平台)
  • UI 方案:
    • 原生 UI:Android 用 Jetpack Compose,iOS 用 SwiftUI
    • 共享 UI:Compose Multiplatform(跨平台 Compose)

核心概念

┌────────────────────────────────────────────────┐
│                  commonMain                     │
│   expect fun getPlatformName(): String          │  ← 声明接口
├──────────────────────┬─────────────────────────┤
│      androidMain     │        iosMain          │
│   actual fun get..()actual fun get..()   │  ← 各平台实现
│   = "Android"        │    = "iOS"              │
└──────────────────────┴─────────────────────────┘

适合场景

  • 已有 Android 应用,想扩展到 iOS
  • 想保持各平台的原生体验
  • 团队熟悉 Kotlin

不太适合

  • 想一套代码搞定所有 UI
  • 团队对 Kotlin 不熟悉
  • iOS 是主要平台(用 SwiftUI 原生可能更顺)

代码示例

// commonMain - 共享的网络请求逻辑
class UserRepository(private val api: UserApi) {
    suspend fun getUser(id: String): User {
        return api.fetchUser(id)
    }
}

// 在 Android 和 iOS 中都可以直接使用
val repo = UserRepository(api)
val user = repo.getUser("123")

入门步骤

  1. 安装 Android Studio + Kotlin Multiplatform Mobile 插件
  2. 创建 KMP 项目(选择模板)
  3. shared/src/commonMain 中编写共享逻辑
  4. Android 端:直接依赖 shared 模块
  5. iOS 端:通过 CocoaPods 或 Swift Package Manager 集成

2.11 Lynx(ByteDance,2024 开源)

一句话定位:字节跳动的跨端方案,用 Web 语法写原生渲染的 UI。

技术栈

  • 语言:JavaScript/TypeScript
  • UI 语法:类 React/CSS(支持 Flexbox)
  • 渲染:自研原生渲染引擎(非 WebView)

核心特点

  • 双线程架构:UI 线程和 JS 线程分离,避免 JS 阻塞渲染
  • CSS 子集:支持 Flexbox、常用属性,但不是完整 CSS
  • PlatformView:可嵌入原生控件(如地图、视频播放器)

适合场景

  • 前端团队想做高性能移动应用
  • 需要比 RN 更好的动效性能
  • 字节系应用的技术选型

不太适合

  • 追求稳定、成熟的生态
  • 需要社区大量第三方库支持
  • 桌面端需求(目前主要支持移动端 + Web)

代码示例

// Lynx 的语法对 React 开发者很熟悉
import { Component, View, Text, Image } from '@anthropic/lynx';

export default class App extends Component {
  state = { count: 0 };

  render() {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <Text style={{ fontSize: 24 }}>点击了 {this.state.count} 次</Text>
        <View
          style={{ padding: 15, backgroundColor: '#007AFF', borderRadius: 8 }}
          onClick={() => this.setState({ count: this.state.count + 1 })}
        >
          <Text style={{ color: 'white' }}>+1</Text>
        </View>
      </View>
    );
  }
}

入门步骤

  1. 参考官方文档:lynxjs.org/
  2. 安装 Lynx CLI
  3. 创建项目并配置模拟器环境
  4. 运行调试

2.12 Valdi(Snapchat,2024 Beta)

一句话定位:TypeScript 编译成原生视图,追求 TS 开发体验 + 原生性能。

技术栈

  • 语言:TypeScript
  • 编译:TS → 原生视图代码(不是解释执行)
  • 渲染:原生控件

核心理念

  • 不走 WebView,也不走 JS 运行时
  • 把 TS 代码编译成原生代码
  • 类型安全 + 原生性能

适合场景

  • 喜欢 TypeScript 但不想用 WebView
  • 追求原生性能
  • 愿意尝试新技术

不太适合

  • 需要稳定、成熟的生态
  • 大型团队生产环境使用(目前是 Beta)

入门步骤

  1. 访问:github.com/Snapchat/Va…
  2. 按 README 安装工具链
  3. 创建项目并配置目标平台
  4. 开发调试

2.13 Electrobun(2024 早期)

一句话定位:比 Electron 更轻量的桌面方案,用 Bun + 系统 WebView/CEF。

技术栈

  • 语言:TypeScript
  • 运行时:Bun(替代 Node.js)
  • 渲染:系统 WebView 或 CEF(可选)
  • 底层:Zig

与 Electron/Tauri 对比

维度 Electron Tauri Electrobun
后端 Node.js Rust Bun (TS)
学习成本 中(要学 Rust)
包体
成熟度 早期

适合场景

  • 想要比 Electron 轻量,但不想学 Rust
  • 喜欢 Bun 的开发体验
  • 愿意接受早期阶段的风险

入门步骤

  1. 安装 Bun:bun.sh/
  2. 访问:electrobun.dev/
  3. 按文档初始化项目
  4. 开发调试

2.14 Dioxus(Dioxus Labs,2021 / v0.5 2024)【重点推荐】

一句话定位:Rust 版的 React,用 React-like 语法写全平台 UI,Rust 生态的"全能选手"。

技术栈

  • 语言:Rust
  • 语法:类 React Hooks(但是 Rust 宏实现)
  • 渲染:多后端(Web/Desktop/Mobile/TUI)
  • 架构:虚拟 DOM + 响应式

核心优势

优势 说明 独特性
React-like 语法 前端开发者易上手 Rust GUI 中最像 React
多渲染后端 Web/Desktop/Mobile/TUI 一套代码多平台
WASM 性能 接近原生的 Web 性能 比 JS 快 2-10 倍
类型安全 Rust 编译时检查 内存安全 + 线程安全
TUI 支持 终端 UI 独特优势 其他框架都不支持

与其他 Rust GUI 框架的对比

维度 Dioxus GPUI Tauri egui
语法风格 React-like Rust 原生 Web 前端 即时模式
学习曲线 低(前端易上手) 高(需熟练 Rust) 低(会 Web 即可) 中等
移动端支持 开发中 v2 支持 有限
Web 支持 5/5 完整 WASM 5/5 完整 3/5 有限
TUI 支持 5/5 独特优势
组件生态 3/5 成长中 2/5 早期 5/5(npm生态) 3/5
渲染性能 4/5 强 5/5 极强 3/5 中等 4/5 强
成熟度 3/5 成长中 3/5 成长中 4/5 稳定 4/5 稳定

适合场景

  • Rust 技术栈,想做全平台应用
  • 需要 Web(WASM)和桌面共享代码
  • 前端转 Rust 的开发者(熟悉 React)
  • 命令行工具需要 TUI 界面
  • 性能敏感的应用(利用 Rust + WASM)
  • 开源项目(生态正在快速成长)

不太适合

  • 不熟悉 Rust 的团队(学习曲线陡)
  • 需要大量现成组件(生态还在建设中)
  • 生产环境要求极高稳定性(v1.0 还未发布)
  • 移动端是主要平台(移动端支持还在完善)

真实案例

  • Blitz(开源):游戏辅助工具
  • FutureSDR:软件定义无线电框架的 UI
  • 多个开源开发工具和 TUI 应用

代码示例(感受 Rust + React 的组合):

基础计数器

use dioxus::prelude::*;

fn main() {
    dioxus_desktop::launch(App);
}

fn App(cx: Scope) -> Element {
    let mut count = use_state(cx, || 0);

    cx.render(rsx! {
        div {
            style: "display: flex; flex-direction: column; align-items: center; gap: 20px;",
            h1 { "计数器" }
            p {
                style: "font-size: 24px;",
                "点击了 {count} 次"
            }
            button {
                onclick: move |_| count += 1,
                style: "padding: 10px 20px; font-size: 18px;",
                "+1"
            }
        }
    })
}

组件复用(像 React 一样):

// 可复用的 Button 组件
#[component]
fn MyButton<'a>(
    cx: Scope<'a>,
    onclick: EventHandler<'a, MouseEvent>,
    children: Element<'a>,
) -> Element<'a> {
    cx.render(rsx! {
        button {
            class: "custom-button",
            onclick: move |evt| onclick.call(evt),
            children
        }
    })
}

// 使用组件
fn App(cx: Scope) -> Element {
    cx.render(rsx! {
        MyButton {
            onclick: |_| println!("Clicked!"),
            "点击我"
        }
    })
}

异步数据获取(类似 React Query):

use dioxus::prelude::*;

fn App(cx: Scope) -> Element {
    let user_data = use_future(cx, (), |_| async move {
        // 异步请求数据
        reqwest::get("https://api.example.com/user")
            .await?
            .json::<User>()
            .await
    });

    cx.render(match user_data.value() {
        None => rsx! { p { "加载中..." } },
        Some(Ok(user)) => rsx! {
            div {
                h1 { "欢迎, {user.name}" }
                p { "邮箱: {user.email}" }
            }
        },
        Some(Err(e)) => rsx! { p { "错误: {e}" } },
    })
}

多渲染后端示例

// 同一套代码,不同渲染后端

// 1. 桌面应用(WebView)
fn main() {
    dioxus_desktop::launch(App);
}

// 2. Web 应用(WASM)
fn main() {
    dioxus_web::launch(App);
}

// 3. 终端 UI(TUI)
fn main() {
    dioxus_tui::launch(App);
}

// 4. 服务端渲染(SSR)
fn main() {
    let html = dioxus_ssr::render(&App(cx));
    // 返回 HTML 字符串
}

入门步骤

  1. 安装 Rust 和 Dioxus CLI

    # 安装 Rust
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    
    # 安装 Dioxus CLI
    cargo install dioxus-cli
    
  2. 创建项目(自动配置):

    dx new my-app
    # 选择模板:web, desktop, mobile, TUI
    
  3. 开发模式(带热重载):

    cd my-app
    dx serve  # Web
    # 或
    dx serve --platform desktop  # 桌面
    
  4. 构建生产版本

    dx build --release
    

Dioxus 0.5 的重大改进(2024):

  • 信号系统:更简单的状态管理
  • 资源系统:内置异步数据获取
  • 路由系统:完整的客户端路由
  • 服务端组件:支持 SSR 和流式渲染
  • 热重载:开发体验接近 Vite

性能优化技巧

  1. 利用 Rust 的零成本抽象
// 组件会在编译时优化
#[inline(always)]
#[component]
fn FastComponent(cx: Scope) -> Element {
    // 编译器会内联这个组件
}
  1. 使用 memo 避免重渲染
let expensive = use_memo(cx, (dep1, dep2), |(d1, d2)| {
    // 只在 dep1 或 dep2 变化时重新计算
    heavy_computation(d1, d2)
});
  1. WASM 优化
# 构建优化的 WASM
dx build --release --platform web
# 生成的 WASM 包通常只有几百 KB

TUI 应用示例(独特优势):

// 用同样的代码创建漂亮的终端 UI
use dioxus::prelude::*;
use dioxus_tui::Config;

fn main() {
    dioxus_tui::launch_cfg(
        App,
        Config::new().with_rendering_mode(RenderingMode::Ansi),
    );
}

fn App(cx: Scope) -> Element {
    let mut count = use_state(cx, || 0);

    cx.render(rsx! {
        div {
            border_width: "1px",
            padding: "2",
            h1 { "Terminal Counter" }
            p { "Count: {count}" }
            button {
                onclick: move |_| count += 1,
                "Increment"
            }
        }
    })
}

常见坑

  • 生命周期标注

    • Rust 的生命周期可能让新手困惑
    • 使用 Dioxus CLI 生成的模板可以避免大部分问题
  • 异步运行时

    • 需要理解 Rust 的 async/await
    • 建议使用 use_future 而不是手动管理
  • 跨平台样式

    • 不同渲染后端的样式支持不同
    • Web 支持完整 CSS,桌面支持子集

Rust GUI 框架选择指南

需求 推荐框架 理由
Web + 桌面共享代码 Dioxus WASM + 多后端
前端团队用 Rust 后端 Tauri 前后端分离
React 开发者转 Rust Dioxus 语法相似
需要 TUI(终端界面) Dioxus 独特支持
追求极致性能(编辑器) GPUI 为 Zed 设计
嵌入式设备 Slint 轻量级
要 npm 生态 Tauri Web 前端

未来展望

  • 📱 移动端支持:Dioxus Mobile 正在开发,预计 2026 稳定
  • 🎨 组件库:社区正在建设类似 shadcn/ui 的组件库
  • 🔧 开发者工具:DevTools 正在完善,类似 React DevTools

2.15 Slint(SixtyFPS GmbH,2020 / v1.0 2023)

一句话定位:嵌入式和桌面 GUI 框架,填补"Qt 太重,Flutter 太大"的空白。

技术栈

  • 语言:Rust/C++/JavaScript(多语言绑定)
  • UI 语法:自研 DSL(.slint 文件)
  • 渲染:多后端(软件渲染/OpenGL/Skia/Femtovg)
  • 架构:声明式 UI + 响应式属性

核心特点

  1. 极致轻量

    • 适合低端嵌入式设备(MCU、ARM Cortex-M)
    • 包体可以做到 < 300KB(不含资源)
    • 内存占用可控(几 MB 级别)
  2. 多语言支持

    • Rust(一等公民)
    • C++(适合嵌入式团队)
    • JavaScript/Node.js(快速原型)
    • Python(正在开发)
  3. 设计师友好

    • 提供可视化设计工具(Slint UI Designer)
    • 支持热重载
    • 类似 QML 的声明式语法

与 Qt 的对比(嵌入式场景):

维度 Qt (Qt Quick) Slint
最小包体 ~10-20MB ~300KB
内存占用 ~20-50MB ~2-10MB
启动速度
MCU 支持 需要 Qt for MCUs(商业版) 开源版支持
许可证 LGPL/GPL 或商业 GPL/商业(企业版)
学习曲线
工业案例 5/5 极多 3/5 成长中

适合场景

  • 嵌入式设备:智能家居、工业控制面板、车载 HMI(低端)
  • 资源受限环境:老旧设备、单板计算机(树莓派)
  • 快速启动应用:系统工具、启动界面
  • 多语言团队:可以用 Rust/C++/JS 中的任意一种

不太适合

  • 需要复杂动效(Qt/Flutter 更强)
  • 需要大量现成组件(生态还在建设)
  • Web 应用(虽然有 WASM,但不如 Dioxus)
  • 移动端应用(主要是桌面+嵌入式)

真实案例

  • 工业控制面板
  • 智能家居设备 UI
  • 医疗设备界面

代码示例(感受 Slint 的 DSL):

UI 文件.slint 声明式语法):

// counter.slint
import { Button, VerticalBox } from "std-widgets.slint";

export component Counter {
    in-out property <int> counter: 0;

    VerticalBox {
        Text {
            text: "点击了 \{counter} 次";
            font-size: 24px;
        }

        Button {
            text: "+1";
            clicked => {
                counter += 1;
            }
        }
    }
}

Rust 调用

// main.rs
slint::slint! {
    import { Counter } from "counter.slint";
}

fn main() {
    let ui = Counter::new().unwrap();

    // 可以从 Rust 代码访问和修改属性
    ui.set_counter(0);

    // 监听属性变化
    ui.on_counter_changed(|value| {
        println!("Counter changed to: {}", value);
    });

    ui.run().unwrap();
}

C++ 调用(嵌入式团队友好):

// main.cpp
#include "counter.h"

int main() {
    auto ui = Counter::create();

    // C++ API 类型安全
    ui->set_counter(0);

    // 回调
    ui->on_counter_changed([](int value) {
        std::cout << "Counter: " << value << std::endl;
    });

    ui->run();
}

入门步骤

  1. 安装 Slint(Rust 项目):

    cargo new my-app
    cd my-app
    cargo add slint
    
  2. 创建 UI 文件

    # 创建 ui/counter.slint
    mkdir ui
    
  3. 配置 build.rs(自动编译 .slint 文件):

    // build.rs
    fn main() {
        slint_build::compile("ui/counter.slint").unwrap();
    }
    
  4. 运行

    cargo run
    

可视化设计工具

# 安装 Slint UI Designer
cargo install slint-viewer

# 实时预览 .slint 文件
slint-viewer ui/counter.slint

嵌入式示例(软件渲染,适合无 GPU 设备):

use slint::platform::software_renderer::{MinimalSoftwareWindow, RepaintBufferType};

fn main() {
    slint::platform::set_platform(Box::new(MyPlatform::new())).unwrap();

    let ui = Counter::new().unwrap();

    // 渲染到帧缓冲区
    let window = ui.window();
    window.set_size(slint::PhysicalSize::new(800, 480));

    // 自定义事件循环(适合 bare-metal 环境)
    loop {
        slint::platform::update_timers_and_animations();
        window.draw_if_needed(|renderer| {
            // 渲染到你的帧缓冲区
        });
    }
}

常见坑

  • DSL 学习.slint 语法需要学习,但比 QML 简单
  • 组件库有限:标准组件够用,但不如 Qt 丰富
  • 文档:相比 Qt 文档较少,但正在改善

什么时候选 Slint 而不是 Qt?

Slint 如果:

  • ✅ 嵌入式设备资源受限(RAM < 50MB)
  • ✅ 需要快速启动(< 100ms)
  • ✅ 想用 Rust 开发嵌入式 GUI
  • ✅ 对许可证敏感(Qt 商业版很贵)

Qt 如果:

  • ✅ 需要丰富的组件库
  • ✅ 工业级项目,稳定性第一
  • ✅ 团队已经熟悉 Qt
  • ✅ 需要跨平台(包括移动端)

2.16 GPUI(Zed Industries,2024)

一句话定位:Zed 编辑器的 UI 框架,Rust 生态的高性能 GUI 方案。

技术栈

  • 语言:Rust
  • 渲染:GPU 加速,自绘渲染
  • 架构:ECS(Entity-Component-System)风格

核心特点

  • 性能极致——为 Zed 编辑器设计,追求每一帧的流畅
  • Rust 原生——类型安全,内存安全
  • 现代 API——异步优先,响应式

适合场景

  • Rust 团队做桌面应用
  • 对性能有极致追求
  • 愿意投入时间学习

不太适合

  • 不熟悉 Rust 的团队
  • 需要快速出成果
  • 需要成熟的组件库

代码示例

// GPUI 的 Rust 风格 UI
use gpui::*;

struct Counter {
    count: i32,
}

impl Render for Counter {
    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        div()
            .flex()
            .flex_col()
            .items_center()
            .child(format!("Count: {}", self.count))
            .child(
                button("Increment")
                    .on_click(cx.listener(|this, _, _| this.count += 1))
            )
    }
}

入门步骤

  1. 安装 Rust:curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  2. 创建项目:cargo new my_app
  3. 添加 GPUI 依赖
  4. 编写 UI 代码
  5. 运行:cargo run

第三章:横向对比

3.1 核心信息对照表

框架 渲染方式 语言 平台覆盖 生态成熟度 一句话定位
Flutter 自绘 Dart 移动+桌面+Web 5/5 成熟 全能选手,跨端一致性最强
React Native 原生映射 JS/TS 移动为主 5/5 成熟 前端团队的原生应用方案
NativeScript 原生映射 JS/TS+Vue/Angular 移动 3/5 成长中 Vue/Angular 写原生应用
Electron WebView JS/TS 桌面 5/5 成熟 Web 做桌面的事实标准
Qt Quick 自绘 C++/QML 全平台+嵌入式 5/5 成熟 工业级、嵌入式首选
.NET MAUI 原生映射 C# 全平台 4/5 稳定 C# 团队的官方方案
Uno Platform 原生/WASM C# 全平台+Web 4/5 稳定 C# + WebAssembly
Tauri 系统WebView+Rust Rust+Web 桌面+移动 4/5 稳定 轻量级 Electron 替代
Wails 系统WebView+Go Go+Web 桌面 3/5 成长中 Go 技术栈做桌面
KMP 原生/Compose Kotlin 移动+桌面 4/5 稳定 Android 团队扩 iOS
Lynx 自绘 JS/TS 移动+Web 3/5 成长中 高性能+Web语法
Valdi 编译到原生 TypeScript 移动 2/5 早期 TS 编译到原生
Electrobun 系统WebView/CEF TypeScript 桌面 2/5 早期 轻量桌面方案
Dioxus 自绘/多后端 Rust 全平台+TUI 3/5 成长中 Rust 版 React
Slint 自绘 Rust/C++/JS 桌面+嵌入式 3/5 成长中 轻量嵌入式 GUI
GPUI 自绘 Rust 桌面 2/5 早期 Rust 高性能 GUI

3.2 指标矩阵

说明:以下评价基于渲染原理和生态现状的一般判断,实际表现取决于具体实现。评分采用 1-5 分制,5 分最高。

框架 包体/启动 性能上限 原生体验 跨端一致 开发效率 生产风险
Flutter 3 中等 5 极强 3 一般 5 极强 5 极高
React Native 3 中等 4 强 5 极强 3 一般 5 极高
NativeScript 3 中等 4 强 5 极强 3 一般 4 高
Electron 2 大/慢 3 中等 3 一般 5 极强 5 极高
Qt Quick 3 中等 5 极强 3 一般 5 极强 3 中等
.NET MAUI 3 中等 4 强 5 极强 3 一般 4 高
Uno Platform 3 中等 4 强 4 强 4 强 4 高
Tauri 5 小/快 4 强 3 一般 4 强 4 高
Wails 4 小/快 4 强 3 一般 4 强 5 极高
KMP 视UI方案 视UI方案 5 极强 3 一般 4 高
Lynx 4 小/快 5 极强 3 一般 5 极强 4 高
Valdi 4 小/快 5 极强 5 极强 3 一般 3 中等
Electrobun 4 小/快 3 中等 3 一般 4 强 4 高
Dioxus 4 小/快 5 极强 3 一般 4 强 4 高
Slint 5 小/快 4 强 3 一般 5 极强 3 中等
GPUI 4 小/快 5 极强 3 一般 3 一般 3 中等

指标说明

  • 包体/启动:应用包大小和启动速度(5=最小最快,1=最大最慢)
  • 性能上限:复杂动效、大数据量场景的表现潜力(5=极限性能,1=性能受限)
  • 原生体验:与系统控件的融合程度(5=完全原生,1=明显非原生)
  • 跨端一致:不同平台上 UI 的统一程度(5=完全一致,1=差异大)
  • 开发效率:上手速度、调试体验、工具链成熟度(5=极高,1=很低)
  • 生产风险:生态稳定性、长期维护的不确定性(低/中/高)

第四章:场景化选型指南

快速决策表

不想看详细分析?根据你的情况直接查表:

按技术栈选择

你的技术栈 首选 备选 理由
React React Native Lynx 复用 React 技能
Vue/Angular NativeScript Flutter 直接用 Vue/Angular
Go Wails - 唯一的 Go 桌面方案
Rust(有前端) Tauri Dioxus 前后端分离
Rust(纯 Rust) Dioxus GPUI React-like 语法
C# .NET MAUI Uno Platform 微软生态
C++ Qt Slint 工业级/嵌入式
Kotlin KMP Flutter Android 团队扩展

按需求选择

你的需求 推荐框架 原因
极致跨端一致性 Flutter, Qt 自绘渲染
极小包体(< 5MB) Tauri, Slint 系统 WebView/轻量
原生体验优先 React Native, .NET MAUI 原生控件
嵌入式设备 Slint, Qt 资源占用低
需要 WebAssembly Uno Platform, Dioxus 浏览器运行
需要终端 UI(TUI) Dioxus 独特优势
快速原型 Electron, Flutter 工具链成熟

场景 A:移动端为主,重动效、品牌视觉统一

典型产品:电商首页、社交 feed、游戏化应用

推荐:Flutter / Lynx

理由

  • 自绘渲染保证跨端一致性
  • 动效性能有保障
  • Flutter 生态成熟,Lynx 性能更极致(但风险更高)

备选:Qt Quick(如果团队熟悉 C++)


场景 B:桌面应用(按技术栈)

团队技术栈 首选方案 优势 典型产品
纯前端团队 Electron 生态最成熟,工具链完善 VS Code, Slack
前端 + 在意包体 Tauri 包体小(3MB),启动快 系统工具
Go 后端团队 Wails 无需学 Rust,类型安全 运维面板,数据处理
Rust 团队 Tauri 安全性高,插件生态好 开发工具
想尝新 Electrobun Bun 运行时,TS 全栈 原型项目

快速决策

  • 求稳定 → Electron
  • 要轻量 → Tauri
  • 用 Go → Wails
  • 学 Rust → Tauri

场景 C:企业内部应用,C# 团队,长期维护

典型产品:ERP、CRM、内部审批系统

推荐:.NET MAUI

理由

  • 与微软生态(Azure、Office 365)集成好
  • C# 企业级开发经验可复用
  • 长期维护有保障(微软背书)

备选:Qt(如果需要嵌入式支持)/ Electron(如果有 Web 版需求)


场景 D:Android 团队扩展 iOS

典型产品:已有 Android 应用,想扩展到 iOS

推荐:KMP(逻辑共享 + 原生 UI)

理由

  • Kotlin 语言统一,学习成本低
  • 可以渐进式迁移,风险可控
  • 各平台 UI 保持原生体验

进阶:如果想共享部分 UI → KMP + Compose Multiplatform


场景 E:需要深度系统集成

典型产品:文件管理器、系统工具、相机应用

推荐:React Native / KMP(原生 UI)

理由

  • 原生控件映射,系统 API 调用方便
  • 无障碍支持天然继承
  • 可以针对各平台做深度优化

备选:.NET MAUI、纯原生


场景 F:极度关注包体大小

典型产品:Lite 版应用、下沉市场、低端设备

推荐:Tauri(桌面)/ Valdi(移动)

理由

  • Tauri 空项目约 3MB
  • Valdi 编译到原生,无运行时开销

备选:KMP + 原生 UI


场景 G:全平台覆盖(移动 + 桌面 + Web)

典型产品:跨平台协作工具、内容消费应用

推荐:Flutter

理由

  • 唯一真正"一套代码,全平台运行"的成熟方案
  • 移动、桌面、Web 体验一致

备选:Qt(工业场景)、各平台分别开发


场景 H:Rust 技术栈做桌面应用

典型产品:开发工具、性能敏感型应用

推荐

  • 需要 Web 前端:Tauri
  • 需要 Web + 桌面共享代码:Dioxus
  • 纯 Rust,极致性能:GPUI

理由

  • Tauri:前后端分离,前端用熟悉的 Web 技术栈
  • Dioxus:React-like 语法,前端转 Rust 易上手,支持 WASM
  • GPUI:为代码编辑器设计,性能极致但学习曲线陡

选择建议

  • 团队有前端,后端用 Rust → Tauri
  • 想要纯 Rust 技术栈,喜欢 React → Dioxus
  • 追求极致性能(如编辑器)→ GPUI

场景 I:嵌入式设备 GUI

典型产品:智能家居面板、车载 HMI、工业控制、医疗设备

推荐:Slint(首选)/ Qt(工业级)

理由

  • Slint:轻量(< 300KB),支持软件渲染,适合低端 MCU
  • Qt:功能强大,工业案例丰富,但包体大、需商业授权

选择建议

  • 资源极度受限(RAM < 50MB)→ Slint
  • 需要丰富组件库,工业级项目 → Qt
  • 原型验证、Rust 技术栈 → Slint

场景 J:Vue/Angular 团队做移动应用

典型产品:企业内部应用、内容展示应用

推荐:NativeScript

理由

  • 直接用 Vue 或 Angular 写原生应用
  • 无需学 React(如果用 React Native 需要学 React)
  • 直接访问原生 API,无桥接层

备选:Flutter(如果愿意学 Dart)


场景 K:C# 团队,需要 WebAssembly

典型产品:需要 Web 版的企业应用、渐进式 Web 应用

推荐:Uno Platform

理由

  • 同时支持原生平台和 WebAssembly
  • 一套代码可以跑在浏览器里
  • WinUI 语法,Windows 应用迁移方便

备选:Blazor WebAssembly(纯 Web)+ .NET MAUI(原生)


第五章:选型方法论

5.1 三步选型法

Step 1: 确定渲染路线
    │
    ├── 需要跨端视觉完全一致 → 自绘渲染(Flutter/Lynx/Qt)
    ├── 需要原生体验优先 → 原生映射(RN/MAUI/KMP)
    └── 需要快速上线、前端技术栈 → WebView(Electron/Tauri)

Step 2: 确定平台覆盖
    │
    ├── 移动端为主 → Flutter/RN/Lynx/KMP
    ├── 桌面端为主 → Electron/Tauri/Qt/GPUI
    └── 全平台 → Flutter/Qt

Step 3: 匹配团队技能
    │
    ├── Dart → Flutter
    ├── JS/TS + React → React Native / Lynx / Dioxus(想学 Rust)
    ├── JS/TS + Vue/Angular → NativeScript
    ├── JS/TS + 任意框架 → Electron / Tauri / Wails / Electrobun
    ├── C# → .NET MAUI / Uno Platform(需要 WASM)
    ├── C++ → Qt / Slint(嵌入式)
    ├── Go → Wails
    ├── Kotlin → KMP
    └── Rust → Tauri(Web前端) / Dioxus(全栈) / GPUI(纯Rust) / Slint(嵌入式)

5.2 决策检查清单

在最终决定前,问自己这些问题:

基础问题

  • 团队对目标语言的熟悉程度如何?(Dart/JS/TS/C#/C++/Go/Kotlin/Rust)
  • 是否有时间预算来学习新技术?
  • 对包体大小和启动速度的要求有多高?
  • 是否需要与系统功能深度集成?
  • 是否需要跨端 UI 完全一致?
  • 项目周期是多长?是否允许使用新兴框架?
  • 团队规模如何?是否需要大量第三方库支持?
  • 未来是否需要扩展到更多平台?

新增考虑点(针对新框架):

  • 是否是 Go 技术栈?考虑 Wails
  • 是否需要 WebAssembly 支持?考虑 Uno Platform / Dioxus
  • 是否是 Vue/Angular 技术栈?考虑 NativeScript
  • 是否是嵌入式设备(RAM < 50MB)?考虑 Slint
  • 是否想用 Rust 写全栈(包括 UI)?考虑 Dioxus
  • 是否需要终端 UI(TUI)?考虑 Dioxus
  • 是否追求极致性能(如代码编辑器)?考虑 GPUI

第六章:趋势观察

6.1 当前格局(2026 更新)

成熟稳定层(生产环境可放心使用):

  • 移动端:Flutter、React Native
  • 桌面端:Electron、Qt
  • C# 生态:.NET MAUI、Uno Platform
  • 逻辑共享:KMP

快速上升层(已有成功案例,值得认真考虑):

  • 轻量桌面:Tauri、Wails
  • 新兴移动:Lynx(字节跳动背书)
  • Rust 全栈:Dioxus(社区活跃)

新锐探索层(有潜力,需承担早期风险):

  • 桌面端:Electrobun、GPUI
  • 移动端:Valdi
  • 嵌入式:Slint

6.2 趋势预判

  1. 自绘渲染持续演进

    • Flutter 的 Impeller 引擎带来更好的 iOS 性能
    • Dioxus、Slint 等新框架证明自绘渲染仍有创新空间
    • GPU 加速成为标配
  2. Rust 生态全面爆发(重要趋势)

    • 桌面端:Tauri(轻量)、Dioxus(全栈)、GPUI(性能)、Slint(嵌入式)
    • Rust 已经形成完整的 GUI 生态矩阵
    • WebAssembly + Rust 成为 Web 高性能方案
    • 预测:2026-2027 会有更多 Rust GUI 框架成熟
  3. WebView 方案的"语言多样化"

    • 传统:Electron(Node.js)
    • 新势力:Tauri(Rust)、Wails(Go)、Electrobun(Bun)
    • 趋势:每个后端语言都会有自己的 WebView 方案
    • Go、Rust、Bun 的学习曲线比 Node.js 低(或类型更安全)
  4. 逻辑共享成为共识

    • 即使 UI 不共享,业务逻辑共享也成为趋势
    • KMP 模式证明了渐进式迁移的可行性
    • Dioxus 的多渲染后端也是类似思路
  5. WebAssembly 的崛起

    • Uno Platform 证明了 C# + WASM 的可行性
    • Dioxus 的 WASM 性能接近原生
    • 预测:更多框架会支持 WASM 作为部署目标
  6. 类型安全成为标配

    • Wails 的自动生成 TypeScript 类型
    • Dioxus 的 Rust 类型安全
    • Slint 的多语言类型绑定
    • 趋势:前后端通信的类型不匹配会成为历史
  7. 前端框架语法的多样化

    • 不再是"React 一家独大"
    • NativeScript 支持 Vue/Angular
    • Dioxus 带来 Rust + React-like 语法
    • 趋势:每个前端生态都能找到对应的跨平台方案
  8. 嵌入式 GUI 的轻量化

    • Slint 证明了 Qt 不是嵌入式唯一选择
    • 软件渲染 + 极致优化可以跑在 MCU 上
    • 趋势:智能家居、车载等场景会有更多轻量方案

总结

选框架不是选"最好的",而是选"最适合的"。

如果你只记住一件事,那就是:

先想清楚你的核心诉求是什么——跨端一致性、原生体验、还是开发效率?然后在对应的技术路线里,选一个匹配团队技能的框架。

2026 关键变化总结

变化 具体表现 影响
1. 桌面方案多元化 Electron/Tauri/Wails/Electrobun 每个后端语言都有选择
2. Rust GUI 成熟 Tauri/Dioxus/Slint/GPUI 覆盖全场景
3. 前端多样化 React/Vue/Angular 都有方案 不再是 React 独大
4. WASM 普及 Uno/Dioxus 支持 浏览器运行原生性能
5. 嵌入式轻量化 Slint 挑战 Qt 低端设备新选择

选型建议(按风险偏好)

稳妥派(生产环境)
├─ 移动端:Flutter, React Native
├─ 桌面端:Electron, Qt
└─ C# 生态:.NET MAUI, Uno Platform

平衡派(值得尝试)
├─ Go 桌面:Wails
├─ Rust 全栈:Dioxus
└─ 逻辑共享:KMP

激进派(原型/小项目)
├─ Lynx, Valdi(移动端新思路)
├─ Electrobun(桌面 Bun 方案)
└─ Slint(嵌入式轻量)

祝选型顺利!


参考资源

官方文档

成熟框架

新兴框架

延伸阅读

对比文章

生产实践案例

技术深度解析

沪市首份年报出炉,芯导科技2025年净利润1.06亿元

2026年2月2日 20:24
沪市首份年报出炉,芯导科技2月2日披露的2025年年度报告显示,公司2025年实现营业收入3.94亿元,同比增长11.52%;归属于上市公司股东的净利润1.06亿元,同比下降4.91%;基本每股收益0.9元。公司拟向全体股东每10股派发现金红利4.3元(含税)。(证券时报)
❌
❌