标签 Vite 下的文章

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-10msDOM 操作
总计(首次)~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" 的文档有 30 个
  IDF("React") = log(100/30) = 0.52
  TF-IDF("React", A) = 0.1 × 0.52 = 0.052

- "Hooks" 出现 5 次
  TF("Hooks", A) = 5/100 = 0.05
  包含 "Hooks" 的文档有 5 个
  IDF("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
Algolia0 (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. 多语言分词:支持中英文等智能分词

相关资源

TinyProTinyEngine 是 OpenTiny 开源生态的重要组成部分:

  • TinyPro 提供企业级后台系统模板
  • TinyEngine 提供灵活强大的低代码引擎

本项目在 TinyPro 中深度集成了基于 TinyEngine 的低代码设计器,通过 插件化架构 构建出可扩展的低代码开发平台。

借助它,你只需在可视化设计器中完成页面设计,就能一键导入 TinyPro,并自动生成菜单、权限及国际化配置,实现真正的 “所见即所得” 式开发体验。

整体架构

lowcode-designer/
├── src/
│   ├── main.js              # 应用入口
│   ├── composable/          # 可组合逻辑
│   ├── configurators/       # 配置器
├── registry.js              # 插件注册表
├── engine.config.js         # 引擎配置
└── vite.config.js          # 构建配置

image.png

核心组成部分

  1. TinyEngine 核心:提供低代码设计器的基础能力
  2. 插件系统:通过插件扩展功能
  3. 注册表机制:统一管理插件和服务
  4. 配置器系统:自定义组件属性配置

核心特性

  • 智能代码生成:基于可视化设计自动生成符合 TinyPro 规范的 Vue 3 + TypeScript 代码
  • 🔐 自动认证管理:智能获取和管理 API Token,支持多种认证方式
  • 🎯 一键集成:自动创建菜单、配置权限、添加国际化词条
  • 🛠️ 代码转换:将 TinyEngine 生成的代码自动转换为 TinyPro 项目兼容格式
  • 💾 本地保存:支持将生成的文件保存到本地文件系统
  • 🎨 可视化配置:提供友好的 UI 界面进行菜单和路由配置

快速开始

安装

使用 TinyCli 可以快速初始化 TinyPro 模版

tiny init pro 

image 1.png

启动低代码设计器

cd lowcode-designer
pnpm install
pnpm dev

启动前端与后端

cd web
pnpm install
pnpm start

cd nestJs
pnpm install
pnpm start

启动完成后,访问 👉 http://localhost:8090 即可体验低代码设计器。

使用流程

image 2.png

设计页面:在 TinyEngine 可视化编辑器中设计页面

image 3.png

点击出码按钮:点击工具栏中的”出码”按钮

image 4.png

配置菜单信息:在弹出的对话框中填写菜单配置信息

生成预览:点击”生成预览”查看将要生成的文件

image 5.png

完成集成:点击”完成集成”自动创建菜单、分配权限并保存文件

image 6.png

接下来我们就可以直接去 TinyPro 直接看到页面效果

image 7.png

TinyPro Generate Code 插件解析

插件目录结构

generate-code-tinypro/
├── package.json              # 插件包配置
├── src/
│   ├── index.js             # 插件入口
│   ├── meta.js              # 元数据定义
│   ├── Main.vue             # 主组件
│   ├── SystemIntegration.vue # 功能组件
│   ├── components/          # 通用组件
│   │   ├── ToolbarBase.vue
│   │   ├── ToolbarBaseButton.vue
│   │   └── ToolbarBaseIcon.vue
│   ├── composable/          # 可组合逻辑
│   │   ├── index.js
│   │   └── useSaveLocal.js
│   └── http.js              # HTTP 服务
├── vite.config.js           # 构建配置
└── README.md                # 文档

代码生成流程

const generatePreview = async () => {
  // 1. 获取当前页面的 Schema
  const currentSchema = getSchema();

  // 2. 获取应用元数据(i18n、dataSource、utils等)
  const metaData = await fetchMetaData(params);

  // 3. 获取页面列表和区块信息
  const pageList = await fetchPageList(appId);
  const blockSchema = await getAllNestedBlocksSchema();

  // 4. 调用代码生成引擎
  const result = await generateAppCode(appSchema);

  // 5. 过滤和转换生成的代码
  const transformedFiles = filteredFiles.map((file) => ({
    ...file,
    fileContent: transformForTinyPro(file.fileContent),
  }));
};

TinyPro 与 TinyEngine 通信

当用户在低代码设计器中点击“完成集成”时,插件首先通过 Token Manager 向认证接口 /api/auth/api-token 请求并获取访问凭证(Token),随后利用该 Token 调用一系列后台接口,包括国际化 API、菜单 API 和角色 API。插件通过这些接口自动完成 页面国际化词条创建、菜单注册、角色查询与权限分配 等步骤。整个过程中,HTTP Client 统一负责与后端通信,而返回的数据(菜单信息、角色信息、权限配置等)会实时更新到本地,最终实现了从页面设计到系统集成的一键闭环,使 TinyEngine 生成的页面能无缝接入 TinyPro 系统。

image 8.png

总结

通过 TinyPro 与 TinyEngine 的深度融合,我们实现了从「可视化设计」到「系统集成」的完整闭环,让不会写代码的用户也能轻松构建出高质量的前端页面

用户只需拖拽组件、填写配置、点击“出码”,插件便会自动生成符合 TinyPro 标准的代码,并完成菜单、权限、国际化等系统级配置。

这一过程无需手动修改代码或后台配置,就能一键完成页面创建、接口绑定与权限分配,实现真正意义上的「低门槛、高效率、可扩展」的前端开发体验。

关于OpenTiny

欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~
OpenTiny 官网:https://opentiny.design
OpenTiny 代码仓库:https://github.com/opentiny
TinyPro 源码:https://github.com/opentiny/tiny-pro
TinyEngine 源码: https://github.com/opentiny/tiny-engine

欢迎进入代码仓库 Star🌟TinyPro、TinyEngine、TinyVue、TinyNG、TinyCLI、TinyEditor~
如果你也想要共建,可以进入代码仓库,找到 good first issue 标签,一起参与开源贡献~

Vue3 核心知识点读书笔记

一、Vue 核心原理与架构

1. MVVM 核心模式(核心架构)

Vue 基于 MVVM 模式设计,核心是实现视图与数据的解耦,三者关系如下:

模块核心职责
Model数据层,负责业务数据处理(纯数据,无视图交互逻辑)
View视图层,即用户界面(仅展示内容,不处理数据逻辑)
ViewModel桥梁层,连接 View 和 Model,包含两个核心能力:
✅ DOM Listeners:监听 View 中 DOM 变化,同步到 Model
✅ Data Bindings:监听 Model 中数据变化,同步到 View
关键:View 和 Model 不能直接通信,必须通过 ViewModel 中转,实现解耦。

2. Vue 核心特性(四大核心)

特性具体说明示例/应用场景
数据驱动视图数据变化自动触发视图重新渲染,无需手动操作 DOM修改变量值 → 页面自动更新
双向数据绑定视图变化 ↔ 数据变化双向同步表单输入框内容自动同步到数据变量
指令分内置指令(Vue 自带)和自定义指令,以v-开头绑定到 DOM 元素v-bind(单向绑定)、v-if(条件渲染)、v-for(列表渲染)
插件支持扩展功能,配置简单VueRouter(路由)、Pinia(状态管理)

二、Vue 版本与开发环境

1. Vue2 vs Vue3 核心差异

维度Vue3 变化
新增功能组合式(Composition)API、多根节点组件、底层渲染/响应式逻辑重构(性能提升)
废弃功能过滤器(Filter)、$on()/$off()/$once() 实例方法
兼容性兼容 Vue2 绝大多数 API,新项目推荐直接使用 Vue3

2. 开发环境准备(必装)

  1. 编辑器:VSCode → 安装「Vue (Official)」扩展(提供代码高亮、语法提示)
  2. 运行环境:Node.js(官网下载安装,为包管理工具提供基础)
  3. 包管理工具:npm/yarn(管理第三方依赖,支持一键安装/升级/卸载,避免手动下载解压)

三、Vite 创建 Vue3 项目(核心操作)

1. 项目创建命令(适配 npm10 版本)

# Yarn 方式(推荐)
yarn create vite hello-vite --template vue

# 交互提示处理(关键步骤,不要遗漏):
# 1. 提示 "Use rolldown-vite (Experimental)?" → 回车选 No(优先使用稳定版)
# 2. 提示 "Install with yarn and start now?" → 回车选 Yes(自动安装依赖并启动项目)

2. 手动创建命令(补充)

# npm 方式
npm create vite@latest
# yarn 方式
yarn create vite
# 后续需手动填写项目名称、选择框架(Vue)、选择变体(JavaScript)

四、Vue3 项目核心文件与目录

1. 项目目录结构(重点关注)

hello-vite/          # 项目根目录
├── node_modules/    # 第三方依赖包(自动生成)
├── dist/            # 构建产物(执行 yarn build 后生成,用于部署)
├── src/             # 源代码目录(开发核心)
│   ├── assets/      # 静态资源(图片、样式等)
│   ├── components/  # 自定义组件
│   ├── App.vue      # 根组件
│   ├── main.js      # 项目入口文件
│   └── style.css    # 全局样式
├── index.html       # 页面入口文件
└── package.json     # 项目配置(依赖、脚本命令)

2. 核心文件代码解析(带完整注释)

(1)index.html(页面入口)
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>hello-vite</title>
  </head>
  <body>
    <!-- Vue 实例挂载容器:被 main.js 中的 Vue 实例控制 -->
    <div id="app"></div>
    <!-- type="module":启用 ES6 模块化语法,引入项目入口文件 -->
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
(2)src/main.js(项目入口,创建 Vue 实例)
// 从 Vue 中导入创建应用实例的核心函数
import { createApp } from 'vue'
// 导入全局样式文件
import './style.css'
// 导入根组件(App.vue)
import App from './App.vue'

// 方式1:简洁写法(创建实例 + 挂载到 #app 容器)
createApp(App).mount('#app')

// 方式2:分步写法(更易理解,效果一致)
// const app = createApp(App) // 创建 Vue 应用实例
// app.mount('#app') // 挂载实例(仅可调用一次)
(3)src/App.vue(根组件,单文件组件核心)
<!-- script setup:Vue3 组合式 API 语法糖,简化组件编写 -->
<script setup>
// 导入子组件(HelloWorld.vue)
import HelloWorld from './components/HelloWorld.vue'
</script>

<!-- template:组件模板结构(视图部分) -->
<template>
  <div>
    <a href="https://vite.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
      <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
    </a>
  </div>
  <!-- 使用子组件,传递 msg 属性 -->
  <HelloWorld msg="Vite + Vue" />
</template>

<!-- style scoped:样式仅作用于当前组件(通过 Hash 隔离,不影响子组件) -->
<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

五、核心知识点总结

1. 核心原理

  • Vue 基于 MVVM 模式,通过 ViewModel 实现视图与数据的双向驱动,核心是「数据驱动视图」,无需手动操作 DOM;
  • 双向数据绑定是 Vue 核心特性,表单场景下可自动同步视图与数据。

2. 项目开发

  • Vue3 推荐使用 Vite 创建项目(比 VueCLI 更快),npm10 版本下优先用 yarn create vite 项目名 --template vue 命令;
  • 项目核心文件:index.html(页面入口)→ main.js(创建 Vue 实例)→ App.vue(根组件),三者构成项目基础骨架。

3. 关键注意点

  • mount() 方法仅可调用一次,挂载目标可以是 DOM 元素或 CSS 选择器(#app/.app);
  • <style scoped> 样式仅作用于当前组件,避免样式污染;
  • Vue3 废弃了过滤器、$on/$off/$once 等功能,开发时需避开。

在 Vite + React 项目中集成 Sentry 完整指南

📚 本教程将带你从零开始,一步步完成 Sentry 在 Vite + React 项目中的接入与配置,实现错误监控与源码映射功能。

📋 目录

3. 创建 Project

在组织中创建一个新项目:

配置项说明
Platform选择 React
Project name填写项目名称(如:vite-react-app
⚠️ 重要提示:创建完成后,请复制并保存好以下 DSN 地址,后续配置会用到:
https://4352b88f3321d7e98766b0b743fa7115@o4514356086109909.ingest.us.sentry.io/4510743223411211


三、初始化本地项目

使用 Vite 创建一个新的 React 项目:

npm create vite@latest my-react-app -- --template react

四、安装 Sentry 依赖

在项目根目录执行以下命令安装 Sentry 相关包:

npm install @sentry/react @sentry/vite-plugin
包名说明
@sentry/react浏览器错误捕获 + React Error Boundary
@sentry/vite-pluginSource Map 上传插件(关键组件)

五、配置 Sentry

1. 创建 Sentry 初始化文件

src 目录下新建 sentry.ts 文件:


import * as Sentry from "@sentry/react";
export function initSentry() {
  // 本地开发环境不自动上报,避免污染
  if (import.meta.env.DEV) {
    return;
  }
  Sentry.init({
    dsn: import.meta.env.VITE_SENTRY_DSN,
    integrations: [
      Sentry.browserTracingIntegration(),
    ],
    // 性能追踪采样率,生产环境建议 0.1 ~ 0.3
    tracesSampleRate: 1.0,
    
    // 设置环境标识
    environment: import.meta.env.MODE,
    // 在发送前可对事件进行脱敏处理
    beforeSend(event) {
      return event;
    },
  });
}

2. 在入口文件中初始化 Sentry

修改 main.tsx注意:必须在 React 渲染之前初始化 Sentry


import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { initSentry } from "./sentry";
// 初始化 Sentry(必须在渲染前调用)
initSentry();
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);

3. 配置环境变量

在项目根目录新建 .env.production 文件:


SENTRY_ORG=my-company-j3
SENTRY_PROJECT=vite-react-web
VITE_SENTRY_DSN=https://4352b88f3321d7e98766b0b743fa7115@o4514356086109909.ingest.us.sentry.io/4510743223411211
SENTRY_AUTH_TOKEN=<从Sentry控制台获取>
SENTRY_RELEASE=my-react-app@0.0.0
环境变量说明
变量名说明获取方式截图
SENTRY_ORG组织标识SettingsOrganizationGeneral Settings
SENTRY_PROJECT项目标识SettingsOrganizationProjects<项目名称>ProjectSlug
VITE_SENTRY_DSN数据源名称创建项目时显示的 DSN
SENTRY_AUTH_TOKEN认证令牌见下文「设置 Auth Token
SENTRY_RELEASE发布版本号根据实际项目填写(如:my-react-app@1.0.0

六、配置 Vite 插件

修改 vite.config.ts,配置 Sentry 插件以实现 Source Map 上传:


import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import { sentryVitePlugin } from '@sentry/vite-plugin'
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '')
  return {
    build: {
      sourcemap: true,
    },
    define: {
      'import.meta.env.VITE_SENTRY_RELEASE': JSON.stringify(env.SENTRY_RELEASE),
    },
    plugins: [
      react(),
      sentryVitePlugin({
        org: env.SENTRY_ORG,
        project: env.SENTRY_PROJECT,
        authToken: env.SENTRY_AUTH_TOKEN,
        sourcemaps: {
          disable: 'disable-upload',
        },
        release: {
          name: env.SENTRY_RELEASE,
          uploadLegacySourcemaps: [
            {
              paths: ['dist/assets'],
              urlPrefix: '~/assets',
            },
          ],
          setCommits: false,
        },
      }),
    ],
  }
})
💡 提示:该配置解决了「在 Sentry 中看不到源码」的问题,通过上传 Source Map 可以精确定位到出错的具体代码行。

七、测试错误上报

修改 App.tsx,添加手动触发错误的按钮,方便测试:


import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import * as Sentry from '@sentry/react'
function App() {
  const [count, setCount] = useState(0)
  const onClickFn = () => {
    console.log('手动触发错误')
    try {
      throw new Error('手动触发错误')
    } catch (err) {
      Sentry.captureException(err)
    }
  }
  return (
    <>
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <button onClick={() => Sentry.captureMessage('消息1')}>
          消息1
        </button>
        <button onClick={onClickFn}>
          手动触发错误
        </button>
        <p>
          Edit <code>src/App.jsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}
export default App

八、设置 Auth Token

该步骤只需操作一次,用于获取 Source Map 上传权限。

1. 进入 Token 创建页面

路径:SettingsDeveloper SettingsPersonal Tokens

2. 创建新 Token

点击「Create New Token」,按以下配置权限:

权限类别选项
ProjectRead
ReleaseAdmin
OriganizationRead
其他选项保持默认 No Access

3. 复制 Token

创建成功后,立即复制生成的 Token(仅显示一次),将其填入 .env.production 文件中的 SENTRY_AUTH_TOKEN


九、构建与验证

1. 构建项目并上传 Source Maps

npm run build
# 或
yarn build

构建完成后,Sentry 插件会自动将 Source Maps 上传到 Sentry 服务器。

2. 预览项目

npm run preview
# 或
yarn preview

3. 触发错误

在浏览器中打开应用,点击「手动触发错误」按钮。如果一切配置正确,你应该能在 Sentry 中看到一条新的错误记录。


十、查看错误日志

在 Sentry 中查看上报的错误

登录 Sentry 官网,进入对应项目,即可看到捕获的错误日志,并且能够直接定位到源码,极大地方便了问题排查。

查看 Source Maps 上传情况

  1. 找到你的项目:SettingsOrganizationProjects<项目名称>

  1. 进入 Source Maps 页面:ProcessingSource Maps
    在这里你可以确认 Source Maps 是否成功上传。


🎉 总结

恭喜!你已经成功完成了 Sentry 在 Vite + React 项目中的完整配置。现在你的应用具备了:

功能说明
🔍 错误捕获自动捕获并上报运行时错误
📍 源码定位通过 Source Map 精确定位错误行号
📊 性能追踪可选的性能数据采样收集
🛡️ 环境隔离本地开发环境不污染生产数据

常见问题

<details>
<summary>❓ 为什么在 Sentry 中看不到源码?</summary>
请检查以下几点:

  1. vite.config.ts 中是否正确配置了 sentryVitePlugin
  2. .env.production 中的 SENTRY_AUTH_TOKEN 是否正确
  3. 构建时是否成功执行(查看控制台是否有上传日志)
    </details>
    <details>
    <summary>❓ 本地开发环境会上报错误吗?</summary>
    不会。在 sentry.ts 中我们添加了环境判断,只有非开发环境才会初始化 Sentry。

    if (import.meta.env.DEV) {
      return;
    }

    </details>

    希望这篇教程对你有所帮助!如有问题,欢迎交流讨论。

本文由mdnice多平台发布

仓库地址:GitHub - zh-lx/code-inspector: 🚀 Click the dom to open your IDE and position the cursor at dom's source code location! 点击页面 dom 来打开 IDE 并将光标自动定位到源代码位置!

一键定位代码

我开发的一个插件,可以在 vite/webpack/turbopack/rspack 等打包工具内引入,当按住插件设定的组合键时,鼠标在页面移动时,就会在 UI 上显示一个遮罩层,点击一下就可以自动打开 IDE 并定位到相关代码。

配合 cc、codex 使用

对于使用 cc、codex 等 vibe coding,不怎么手敲代码的同学,右键点击时直接复制代码的路径,按【option + shift + z】(windows 是【Alt + shift + z】),可以打开模式设置,将 Locate Code 关掉,打开 Copy Path。这样点击时就是复制 UI 所在的文件、行、列,告诉 cc、codex 进行修改时就可以更快地处理,节省 token 了。

如何接入使用

接入很简单,只需要安装依赖配置一下就行,在 github 的 README 中 介绍比较详细,就不在这里过多赘述了

原来不少知名 AI 项目也有我的身影?

今天看了下 dependents,发现原来 dify、cherry-studio 和 new-api 等开源项目都有在用我的插件,原来我也偷摸为 AI 开源做了一些贡献


📌 转载信息
转载时间:
2026/1/20 17:39:21

佬友们好!作为一个经常在终端里敲命令的开发者,我相信大家都设置过各种 alias 别名来提高效率。但每次都要手动编辑

.zshrc

.bashrc

,是不是觉得有点麻烦?

今天给大家分享一个我写的小工具 —— AliasGUI,一个跨平台的可视化 Shell 别名管理器!

痛点

  • 每次添加别名都要
vim ~/.zshrc

,然后找到正确位置添加

  • 手动编辑容易出错,比如等号两边加了空格
  • 同时在 macOS 和 Windows 上工作,语法还不一样(Bash vs PowerShell)
  • 别名越来越多,管理起来一团乱麻
  • 换电脑后又要重新配置一遍

AliasGUI 功能

软件截图




核心特性

功能描述
跨平台支持 macOS、Windows、Linux
可视化管理图形界面操作,无需编辑配置文件
快速搜索即时搜索已有别名
备份恢复一键备份,随时恢复,再也不怕手残删错
智能检测自动检测系统 Shell 和配置文件
安全保护只管理 AliasGUI 区块,保留你原有的配置

使用方法

  1. 添加别名:点击 + 新增 按钮,输入别名名称和命令
  2. 保存生效:点击 保存并生效 按钮
  3. 让别名生效
  • macOS/Linux:
source ~/.zshrc
  • Windows: 重新打开 PowerShell

技术栈

  • Electron - 跨平台桌面应用框架
  • React - 前端 UI 框架
  • Vite - 构建工具
  • Node.js - 后端服务

GitHub 开源

项目地址

欢迎 Star、提 Issue 和 PR!

最后

如果有任何建议或者发现 bug,欢迎在评论区告诉我,或者直接在 GitHub 上提 Issue。

感谢各位佬的阅读!


📌 转载信息
原作者:
hyojoo
转载时间:
2026/1/19 18:06:50

解决什么问题

想要使用本地 monorepo 开发 + 纯 npm 管理的项目,不想对项目进行 pnpm 改造的开发者

项目地址

https://github.com/alamhubb/mono

Mono

零侵入式 Monorepo 开发工具

直接使用 TypeScript 源码开发,无需构建,无需改造项目


什么是 Mono?

Mono 是一套零侵入式 monorepo 开发工具。它允许你在开发期间直接使用 TypeScript 源码,无需构建包或重构项目。

pnpm workspace 的「链式污染」问题

使用 pnpm workspace 意味着整条依赖链都被迫使用 pnpm

项目 A (pnpm) → 必须用 pnpm
  └── 项目 B → 必须用 pnpm(被污染)
        └── 项目 C → 必须用 pnpm(被污染)
              └── 项目 D → 必须用 pnpm(被污染)
  • 所有相关项目都必须改成 pnpm
  • 所有开发人员都必须安装 pnpm
  • 所有 CI/CD 环境都必须配置 pnpm
  • 新成员 npm install 直接报错:workspace:*

详细分析请阅读:「链式污染」—— 为什么一个 pnpm 项目会逼着整条依赖链都改成 pnpm

Mono 的解决方案

使用 Mono,你只需:

  • 运行 mono ./src/index.ts - 就这么简单!
  • 无需重构项目,无需 workspace:*
  • 无需构建包,直接使用源码
  • 修改立即生效,npm/yarn/pnpm 都兼容

pnpm workspace vs Mono

方面pnpm workspaceMono
安装必须安装 pnpm可选
配置文件需要 pnpm-workspace.yaml不需要
package.json需要修改为 workspace:*不需要修改
克隆后使用必须 pnpm installnpm/yarn/pnpm 都可以
依赖包需要先构建直接使用源码
团队协作所有人必须用 pnpm不强制


包列表

本仓库包含两个协同工作的包:

用途安装
mono-mjsNode.js CLI - 用于构建工具、Vite 插件npm install -g mono-mjs
vite-plugin-monoVite 插件 - 用于浏览器运行时npm install -D vite-plugin-mono

何时使用哪个?

场景工具
运行脚本、构建工具mono
Vite 插件、编译器mono
浏览器端导入vite-plugin-mono
Vue/React 组件vite-plugin-mono


快速开始

1. 全局安装 CLI

npm install -g mono-mjs

2. 运行项目

# 直接运行 TypeScript
mono ./src/index.ts

# 使用本地包运行 Vite
mono ./node_modules/vite/bin/vite.js

3. (可选)添加 Vite 插件用于浏览器端

npm install -D vite-plugin-mono
// vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { viteMono } from 'vite-plugin-mono' export default defineConfig({
  plugins: [
    viteMono(),  // 放第一个! vue()
  ]
})


特性

  • 零侵入 - 无需重构项目,无需配置文件
  • 自动发现 - 递归查找所有本地包
  • 即时生效 - 修改立即生效
  • 包管理器无关 - 支持 npm、yarn、pnpm、bun
  • 零配置 - 默认 ./src/index.ts,可选 local 字段


工作原理

包发现

直线向上查找距离最远的项目根目录 (.idea/.vscode/.git/package.json)
  └── 递归扫描
      └── 查找所有 package.json
          └── 根据 "name" 字段注册

导入拦截

// 你的代码 import { utils } from 'my-utils' // Mono 重定向到源码 // → /path/to/my-utils/src/index.ts 


配置

零配置(默认)

所有包默认使用 ./src/index.ts。无需任何配置!

自定义入口(可选)

package.json 中添加 local 字段:

{ "name": "my-package", "local": "./src/main.ts" } 


环境要求

  • Node.js >= 18.19.0
  • ESM 项目 - package.json 中需要 "type": "module"



📌 转载信息
转载时间:
2026/1/14 18:00:12

话不多说,直接上效果图!

网站地址:VersaTool 极效工具箱

已实现功能

文本对比、高频统计、大小写转换、字数统计、假文生成器、时间戳转换、二维码生成、JSON 格式化、模拟数据生成、Base64 编解码、强密码生成、加密工坊。

开发流程:

1. 使用 Google AI Studio 中的 Build 功能开发项目应用
2. 在 Trae 里导入完整代码,继续优化项目或修复 Bug
3. 本地运行打包生成静态文件
4. 部署到 Cloudflare Pages 上

对项目感兴趣的佬友,可以在此基础上进行二次开发!

补充一下:浏览器在浅色背景下边框显示不出来,有知道的佬吗?


📌 转载信息
原作者:
zII
转载时间:
2026/1/3 12:02:19