普通视图

发现新文章,点击刷新页面。
今天 — 2025年8月25日掘金 前端

尤雨溪力荐!Vue3 专属!100+ 动效组件!

2025年8月25日 12:25

今年 1 月,尤雨溪在推特上亲自安利了一款「Vue 专属动画库」——Motion Vue

它基于 Framer Motion 的语法,让 Vue 3 开发者也能写出丝滑高性能的动画。

然而官网长期缺案例、少更新,一度被贴上“可能烂尾”的标签。

就在大家快要遗忘之际,Motion 官方突然放了个大招

正式把 Vue 纳入第一梯队,与 ReactJavaScript 并列,成为官方支持的三大平台之一;

并且把原来只在 React 生态才有的 100+ 经典动效示例几乎 1:1 迁移到了 Vue 端

除此之外,官方还补齐了「开发者工具链」——VS Code 可视化编辑器LLM 文档AI 生成 CSS 弹性曲线……一次性打包奉上。

Motion 官方支持平台一览

  • JavaScript
  • React
  • Vue 3(新增)

你还可以在 FramerFigmaSquarespaceWordPressWebflow 等平台通过官方指南直接调用 Motion

100+ 动效组件速览(Vue 已可用)

  • Fade In / Out
    最经典的淡入淡出,一行 :initial + :animate 就能启动。

  • Cursor
    是一个为 Vue 打造的创意光标组件。借助它,你可以快速、轻松地构建自定义光标或跟随鼠标的动画效果。

  • Transform
    创建一个运动值,它将一个或多个运动值的输出进行转换后作为新的结果。

  • Ticker
    Ticker 让你能够快速、轻松地构建无限滚动的跑马灯式动画。

    录屏2025-08-14 23.25.36.gif

  • Typewriter
    打字机效果,可配合 splitText 拆字爆炸。

  • Progress
    进度条加载动画。

更多优秀动画案例请移步官网

  • https://examples.motion.dev/vue

开发工具链「三连击」

  • VS Code 插件
    拖拽调曲线实时预览AI 生成弹簧,动画调试从未如此直观。
    bezier-editor

  • LLM 友好文档
    Cursor / Claude / Windsurf 直接 @motion,API 永不落后。

    文档https://llms.motion.dev

  • Vibe Coding
    FramerFigmav0 已内置 Motion 模板,设计师拖拽 → 开发者导出 Vue

    文档https://motion.dev/docs/tools-quick-start

30 秒上手

npm i motion-v
<motion.div
  :initial="{ backgroundColor: 'rgb(0, 255, 0)', opacity: 0 }"
  :whileInView="{ backgroundColor: 'rgb(255, 0, 0)', opacity: 1 }"
/>

Vue 3 动效生态再添一员猛将

特点 推荐指数
Motion Vue 官方支持、100+ 示例、AI 工具链 ⭐⭐⭐⭐⭐
GSAP 老牌全能、复杂时间轴 ⭐⭐⭐⭐
Anime.js 轻量、易上手 ⭐⭐⭐
Inspira UI 纯 Vue 组件、开箱即用 ⭐⭐⭐⭐
Uiverse.io 社区模板丰富 ⭐⭐⭐

完整 10 款 Vue3 动效库横向对比,可戳下方原文
Vue3 生态:10 个最强大的动效组件库!

写在最后

从**「可能烂尾」「官方亲儿子」**,Motion Vue 只用了一次大更新。
组件全、性能好、工具链完善,还有尤雨溪背书——Vue 3 动画选型,真的可以把它放进第一梯队了。

立即体验

  • 文档 + 100+ 示例https://examples.motion.dev/vue
  • GitHub 仓库https://github.com/motiondivision/motion

Vue3 超强“积木”组件!5 分钟搞定可交互 3D 机房蓝图!

2025年8月25日 12:25

过去,我们用 ExcelVisioCAD 画机房平面图:

  • 改一台机位重新截图重新标注 → 再发邮件。
    如今,一个 不到 200 KB 的 Vue3 组件——Grid Plan v2.0——把“画蓝图”卷进了浏览器,还把图纸 直接立了起来

Grid Plan 是什么?

一句话:
Grid Plan 是一个轻量级 Vue3 组件,用来实时绘制并交互式管理“网格蓝图”——从房间、机柜到数据中心,都能秒级可视化。

“如果 2D 是工程师的语言,那么 3D 就是决策者的母语。”
——Graphieros 团队在 v2.0 发布公告里这样说。

亮点 一句话说明
2D/3D 一键切换 同一套数据,实时生成可旋转、缩放的 3D 视图
完全可插拔 8 个插槽 + BEM 类名,样式和 UI 想怎么改就怎么改
事件驱动 增删改选全部通过事件抛回,业务逻辑零侵入
TypeScript 零配置 所有类型开箱即用,IDE 自动补全

30 秒跑通 Demo

  • 克隆 & 启动
git clone https://github.com/graphieros/grid-plan.git
cd grid-plan
pnpm i          # 或 npm / yarn
pnpm dev        # 本地 5173,带完整 3D 示例
  • 浏览器打开

  • 左侧:可拖拽的组件菜单(服务器 / 电源 / 路由器…)

  • 右侧:实时 3D 机房,鼠标旋转、缩放

  • 任意改动 → 2D3D 同步更新

录屏2025-08-17 22.20.00.gif

核心 API 速查表

维度 关键字段 说明
组件类型 availableTypes 可放置的组件类型清单(含图标、颜色等)
已放置 placedItems 已摆放在蓝图上的组件实例(坐标 + 尺寸)
3D 开关 config.showGrid3d 是否启用 3D 视图
布局 config.grid3dPosition 3D 视图位于 2D 网格的上方或下方
事件 @change / @delete / @select / @created 组件增删改选时触发

把 3D 机房嵌进你的后台

全局注册(main.js)

import { GridPlan } from 'grid-plan'
app.component('GridPlan', GridPlan)

业务页面

<template>
  <GridPlan
    :availableTypes="types"
    :placedItems="layout"
    :config="{ showGrid3d: true, grid3dPosition: 'top' }"
    @change="saveLayout"
    @delete="removeDevice"
  >
    <!-- 自定义清单 -->
    <template #inventory="{ item, deleteItem }">
      <DeviceCard :device="item" @remove="deleteItem" />
    </template>
  </GridPlan>
</template>

<script setup lang="ts">
import type { GridPlanItem, GridPlanItemType } from 'grid-plan'

const types = ref<GridPlanItemType[]>([
  { typeId: 1, description: 'Dell R750', color: '#007DB8', icon: 'server' },
  { typeId: 2, description: 'APC UPS',   color: '#FF6F00', icon: 'power' }
])

const layout = ref<GridPlanItem[]>([])   // 从接口拉取
const saveLayout = (item: GridPlanItem) => api.save(item)
const removeDevice = (item: GridPlanItem) => api.delete(item.id)
</script>

录屏2025-08-17 22.21.07.gif

3D 视图能做什么?

功能 体验亮点
实时同步 2D 改尺寸 → 3D 立即拉伸;3D 拖拽 → 2D 坐标实时变
零额外建模 不需要 glTF/obj,任何矩形组件自动生成立方体
性能怪兽 1000+ 方块 60 FPS,WebGL 局部更新
旋转缩放 鼠标拖拽旋转 / 滚轮缩放 / 触控板双指

样式随便换(Tailwind 示例)

Grid Plan 不带任何 CSS,只暴露类名:

.grid-plan-grid-3d {
  @apply bg-slate-900;
  perspective: 1200px;
}
.grid-plan-inventory__body {
  @apply p-4 bg-slate-800 text-white;
}

写在最后

Excel3D,只需一个 <GridPlan> 标签。
下一次领导再催“机房搬迁方案”,别再熬夜截图 PPT——直接把浏览器投上大屏,让服务器在 3D 里“长”出来!

  • Github 地址https://github.com/graphieros/grid-plan
  • 官网地址https://grid-plan.graphieros.com/

尤雨溪力荐!Vue3 生态最强大的 14 个 UI 组件库!

2025年8月25日 12:24

Vue3 官网 的「EcosystemUI Components」菜单里,官方维护了一份精挑细选的组件库清单:全部基于 Vue3TypeScript 优先、活跃维护、社区认可。

Nuxt UI

  • 亮点Nuxt 3/4 官方旗舰,Reka UI + Tailwind CSS,SSR 满分,全量无障碍、RTL & 暗黑模式、Figma 设计稿
  • 使用场景:需要 SSRSEOi18n 的企业级中后台、营销官网
  • GitHub Stars5.2 k
  • GitHubhttps://github.com/nuxt/ui
  • 官网https://ui.nuxt.com

PrimeVue

  • 亮点Styled / Unstyled 双模式,30+ 主题 + 可视化主题工厂,80+ 复杂组件
  • 使用场景BPMERP、数据密集型后台
  • GitHub Stars13.2 k
  • GitHubhttps://github.com/primefaces/primevue
  • 官网https://primevue.org

Quasar

  • 亮点:同一套代码编译成 SPAPWASSR移动端ElectronCapacitorCLI 工程化完善
  • 使用场景:需要同时交付 WebiOSAndroid 的创业项目
  • GitHub Stars26.8 k
  • GitHubhttps://github.com/quasarframework/quasar
  • 官网https://quasar.dev

Vuetify 3

  • 亮点Material You 动态主题,无障碍满分,636 k/周 npm 下载量,生态最成熟
  • 使用场景:政企、SaaS、严格遵循 Material Design
  • GitHub Stars40.7 k
  • GitHubhttps://github.com/vuetifyjs/vuetify
  • 官网https://vuetifyjs.com

Reka UI

  • 亮点Vue 官方支持的 Headless 内核,零样式、100 % 可定制、TypeScript 友好
  • 使用场景:自建 Design System、需要极致可访问性
  • GitHub Stars5.4 k
  • GitHubhttps://github.com/unovue/reka-ui
  • 官网https://reka-ui.com

Shadcn-vue

  • 亮点:复制粘贴即可用,基于 Reka UI + Tailwind CSS,极客最爱
  • 使用场景:追求最小 bundle、只想要用到的组件
  • GitHub Stars8 k
  • GitHubhttps://github.com/radix-vue/shadcn-vue
  • 官网https://www.shadcn-vue.com

Naive UI

  • 亮点TypeScript 极致友好,主题系统丝滑,中文文档、社区活跃
  • 使用场景中文后台仪表盘SaaS
  • GitHub Stars17.6 k
  • GitHubhttps://github.com/tusen-ai/naive-ui
  • 官网https://www.naiveui.com

Volt UI

  • 亮点PrimeVue Headless + Tailwind 原子类,轻量可摇树
  • 使用场景:喜欢 PrimeVue 功能但想完全自定义样式
  • GitHub Stars:-(跟随 PrimeVue 版本)
  • GitHubhttps://github.com/primefaces/primevue/tree/master/apps/volt/volt
  • 官网https://volt.primevue.org/

Daisy UI

  • 亮点Tailwind CSS 插件,一行类名生成组件,452 k/周下载量
  • 使用场景:原型页、营销落地页、快速 MVP
  • GitHub Stars38.4 k
  • GitHubhttps://github.com/saadeghi/daisyui
  • 官网https://daisyui.com

Flowbite Vue

  • 亮点:与 Flowbite Figma 套件 1:1 对齐,27 个常用组件
  • 使用场景:设计-开发协作紧密、像素级还原
  • GitHub Stars8.8 k
  • GitHubhttps://github.com/themesberg/flowbite-vue
  • 官网https://flowbite-vue.com

Element Plus

  • 亮点:饿了么团队维护,Vue2 无缝迁移,中文生态最完善
  • 使用场景:Vue2 老项目升级、后台 CRUD、权限系统
  • GitHub Stars26.4 k
  • GitHubhttps://github.com/element-plus/element-plus
  • 官网https://element-plus.org

Ant Design Vue

  • 亮点Ant Design 完整设计体系,ProComponents 加持复杂中后台
  • 使用场景:阿里系产品、国际化 SaaS
  • GitHub Stars21.1 k
  • GitHubhttps://github.com/vueComponent/ant-design-vue
  • 官网https://antdv.com

Ark UI

  • 亮点:更轻更快的 Headless 组件库,可 tree-shaking
  • 使用场景:与 Reka UI 类似,但包体更小、API 更简化
  • GitHub Stars4.6 k
  • GitHubhttps://github.com/chakra-ui/ark/tree/main/packages/vue
  • 官网https://ark-ui.com

Vuestic UI

  • 亮点Epicmax 出品,提供即装即用的前端组件,配置简单,能够显著加快响应式、高性能 Web 界面的开发速度,42 个业务组件
  • 使用场景后台系统营销落地页
  • GitHub Stars3.6 k
  • GitHubhttps://github.com/epicmaxco/vuestic-ui
  • 官网https://vuestic.dev

🏁 一页看全

场景需求 首推库
SSR / 静态站点 Nuxt UI
跨端(Web+App) Quasar
纯 Material Design Vuetify
Headless 自建设计系统 Reka UI / Ark UI
复制即用 Shadcn-vue
中文生态 & 文档 Naive UI / Element Plus
阿里系 / 国际化 Ant Design Vue
原型 & 快速 MVP Daisy UI

收藏本文,按图索骥,Vue3 项目选型再也不纠结!

【AI解读源码系列】ant design mobile——Divider分割线

作者 sorryhc
2025年8月25日 11:42

前言

笔者目前业务主要围绕ant design mobile组件库来交付H5前端工作。

故此出此专栏来解读每一个组件是如何实现的。

本文基于AI来解读Divider组件。

文档链接:

mobile.ant.design/zh/componen…

源码:

github.com/ant-design/…

设计prompt

解读antd-mobile divider组件,结构化思维输出整个组件实现流程,按步骤提供关键代码解读 

// 附组件代码

实现步骤


一、属性设计与props合并

Step 1:定义参数和默认值

  • 参数支持:
    • contentPosition: 'left' | 'right' | 'center' —— 分割线中内容的位置(居左/居右/居中)
    • direction: 'horizontal' | 'vertical' —— 分割线方向
    • children —— 可作为分割线中显示内容
    • 继承 NativeProps,支持 className、style、aria等原生属性

关键代码:

const defaultProps = {
  contentPosition: 'center',
  direction: 'horizontal',
}

合并props:

const props = mergeProps(defaultProps, p)
  • 保证所有默认配置被应用,用户传入的参数可覆盖默认值。

二、分割线主结构渲染

Step 2:生成className,确定样式与分布

  • 使用 classnames 工具拼接动态类名,实现样式变化:
    • adm-divider // 基础样式
    • adm-divider-horizontaladm-divider-vertical // 方向
    • adm-divider-center/left/right // 内容位置

关键代码:

className={classNames(
  classPrefix,
  `${classPrefix}-${props.direction}`,
  `${classPrefix}-${props.contentPosition}`
)}
  • 外层是 div,如有 children 内容,包裹一层 ${classPrefix}-content

主结构:

return withNativeProps(
  props,
  <div className={...}>
    {props.children && (
      <div className={`${classPrefix}-content`}>{props.children}</div>
    )}
  </div>
)
  • withNativeProps 是增强原生属性工具,保证你传入的 style/className/aria-xxx 都能自动透传到主div

三、实现分割线效果的关键css

Step 3:CSS实现分割线和内容排布

横向分割线 .adm-divider-horizontal

.adm-divider-horizontal {
  display: flex;
  align-items: center;
  // 线样式
  &::before, &::after {
    flex: auto;
    display: block;
    content: '';
    border-style: inherit;
    border-color: inherit;
    border-width: 1px 0 0;
  }
  .adm-divider-content {
    flex: none;
    padding: 0 16px;
  }
  // 内容位置,限制一侧最大宽度
  .adm-divider-left&::before { max-width: 10%; }
  .adm-divider-right&::after { max-width: 10%; }
}
  • 原理是左右两个伪元素画线,中间内容居左/右/中灵活
  • 线用 border-width: 1px 0 0,就是上方细线
  • 内容用 .adm-divider-content 居中,用flex布局约束

纵向分割线 .adm-divider-vertical

.adm-divider-vertical {
  display: inline-block;
  height: 0.9em;
  margin: 0 16px;
  border-left: 1px solid var(--adm-color-border);
}
  • 细竖线,用 border-left 实现
  • 一般用作行内分割,eg:文字间竖分割

四、内容位置排布实现逻辑

Step 4:维持内容位置left/right/center的切换

  • contentPosition="left",before变短,内容贴左,after拉满剩余
  • right,after变短,内容贴右
  • center,左右线长度一致,内容居中

五、NativeProps原生属性增强

Step 5:保证 className/style/aria-xxx/data-xxx 全部透传出来

return withNativeProps(
  props,
  <div ...>{...}</div>
)
  • 这让 divider 能像原生DOM一样自定义风格和可访问性

以上就是笔者基于AI返回的解读信息稍加了一些补充和修改,结合起来看源码提效真是太多了,对于前端本身就是基于视图所完成编码,因此把组件逻辑层交给AI来解读太适合不过了。

希望对大家有所帮助,共同学习源码。

electron-vite_20配置依赖包运行时区外部加载commonjsExternals_vite-plugin-commonjs-externals

2025年8月25日 11:35

Electron 项目中使用 electron-vite(Vite的Electron 构建工具)时的配置文件,告诉 Vite哪些依赖(包)在打包时不用一起打包,而是运行时让Electron去外部加载;

依赖插件vite-plugin-commonjs-externals

1.项目中版本package.json

"devDependencies":{
"vite-plugin-commonjs-externals": "^0.1.4",
}

2.安装

npm i vite-plugin-commonjs-externals -D

3.引入electron.vite.config.ts 文件

import commonjsExternals from 'vite-plugin-commonjs-externals';

4.使用electron.vite.config.ts

const commonjsPackages = [
  'dingrtc-electron-sdk',
] as const;

export default defineConfig({
main:{},
preload:{},
renderer:{
plugins:[
commonjsExternals({ externals: commonjsPackages }),
]
},
});
  1. 编译后路径
// win电脑右键图标=>属性=>打开文件所在的位置;
// mac公司没有mac有的兄弟可以评论区告诉我怎么弄;
resources/app.asar.unpacked/node_modules/dingrtc-electron-sdk

🔥 uView Pro 全新升级来啦!一行配置,解锁 uView Pro 全局 TS 类型提示与校验

2025年8月25日 11:35

一、前言

TypeScript 作为 Vue3 生态的主流选择,其强大的类型推断和智能提示极大提升了开发效率。而 uView Pro 作为 uni-app 生态下新生的 Vue3 组件库,我们一直致力于为开发者带来更高效、更智能的开发体验。

为了让大家在使用 uView Pro 时也能享受到完善的类型提示和校验,所以近期,uView Pro 新版本正式支持 Volar 插件下的全局组件类型提示与类型校验,大大的提升了 TypeScript 项目的开发效率和代码质量。

接下来,本文将详细介绍 uView Pro 如何集成 Volar 类型提示、配置 tsconfig.json、适配不同项目结构(CLI 与 HBuilderX),并结合实际代码示例,帮助大家快速上手并了解提示和校验原理。

1.gif

二、Volar 与 TypeScript 类型提示

1. 什么是 Volar?

VolarVue 官方推荐的 VSCode 插件,专为 Vue3 + TypeScript 项目设计,提供了更强大的类型推断、智能提示、错误校验和 IDE 体验。相比 VeturVolar<script setup>TSX、全局类型等支持更完善。

2. 为什么要全局类型提示?

Vue3 + TS 项目中,组件库的全局类型提示可以让你在模板中直接获得属性、事件、插槽等智能补全和类型校验,极大减少低级错误和查文档的时间。

三、uView Pro 新版本 TS 类型支持方案

1. 类型声明文件的作用

目前,uView Pro 新版本内置了完整的 TypeScript 类型声明(uview-pro/types),覆盖所有全局组件、props、工具函数等。通过正确配置,开发者可在 IDE 中获得如下编码体验:

  • 组件标签、属性、事件的智能补全
  • 属性类型校验与错误提示
  • 代码跳转与类型追踪
  • 插槽、ref、emit 等 TS 语法无缝支持

2. 配置 tsconfig.json

对于使用 CLI(如 vite、webpack、uni-app cli)方式安装 uView Pro 的项目,需要在 tsconfig.json 中通过 compilerOptions.types 指定类型声明:

// tsconfig.json
{
  "compilerOptions": {
    "types": ["uview-pro/types"]
  }
}

这样,Volar 会自动加载 uView Pro 的全局组件类型声明,无需手动引入。

⚠️ 注意:HBuilderX 项目暂不支持此配置,目前只有 CLI 项目(npm 安装)需手动配置。

3. uni_modules 安装的特殊说明

如果你通过 uni_modules 方式安装 uView Pro(如 uni_modules/uview-pro),则无需任何额外配置,Volar 会自动识别类型声明。

如仍无类型提示与校验,请在 tsconfig.json 中添加以下配置:

// tsconfig.json
{
  "compilerOptions": {
    "typeRoots": ["./src/uni_modules/uview-pro/types"]
  }
}

四、实际开发体验与代码示例

1. 组件类型提示效果

配置完成后,在 .vue 文件模板中输入 <u-,即可获得所有 uView Pro 组件的智能补全:

<template>
  <u-button type="primary" @click="onClick">按钮</u-button>
</template>

<script setup lang="ts">
  function onClick() {
    // ...
  }
</script>

2.png

3.png

4.png

此时,Volar 会自动提示 type@click 等属性和事件类型,错误用法会有红色波浪线提示。

2. 属性类型校验

如果你写错了属性名或类型,Volar 会即时报错:

<u-button type="test" />
<!-- type 不存在 test,Volar 会提示 -->

1.png

3. 事件与插槽类型推断

<u-modal v-model="show" @confirm="onConfirm">
  <template #default>
    <div>内容</div>
  </template>
</u-modal>

Volar 能自动推断 show 的类型、onConfirm 的参数类型,以及插槽内容类型。

5.png

6.png

7.png

4. 工具函数类型提示

目前,uView Pro 工具类有三种使用方式:

  1. 按需导入。
import { deepClone } from "uview-pro";
const obj = { a: 1, b: { c: 2 } };
const copy = deepClone(obj); // copy 类型自动推断

8.png

这样的方式自动 tree-shaking 导入,无需通过 utils 对象间接访问。

  1. 导入 util 对象,即可获得工具函数的类型提示。
import { $u } from "uview-pro";
const obj = { a: 1, b: { c: 2 } };
const copy = $u.deepClone(obj);

10.png

  1. 直接使用 uni. 即可使用,无需导入。
const obj = { a: 1, b: { c: 2 } };
const copy = uni.$u.deepClone(obj);

9.png

5. 结合 <script setup> 的最佳实践

uView Pro 推荐配合 <script setup lang="ts"> 使用,享受最完整的类型推断和代码提示体验。

五、常见问题与排查

1. 为什么没有类型提示?

  • 检查 tsconfig.json 是否正确配置 types 字段。
  • 确认 VSCode 已安装 Volar 插件,并禁用 Vetur。
  • 确认 uView Pro 版本为最新。
  • 重启 VSCode 或重新打开项目。

2. HBuilderX 项目如何获得类型提示?

目前 HBuilderX 不支持 tsconfig.json 的 types 配置,建议使用 CLI 项目获得最佳 TS 体验。

3. 组件未识别或类型报错?

  • 检查组件名称拼写,确保与官方文档一致。
  • 检查依赖版本,升级到最新版本。

六、uView Pro 类型声明实现原理

uView Pro 在源码中为每个组件、工具函数都编写了详细的 d.ts 类型声明,并在 types 目录下集中导出。通过 types 字段,Volar 能自动将这些类型注册为全局组件类型,无需手动 import。

u-button 组件类型声明示例:

// uview-pro/components/u-button/types.ts
export const ButtonProps = {
  /** 是否细边框 */
  hairLine: { type: Boolean, default: true },
  /** 按钮的预置样式,default,primary,error,warning,success */
  type: { type: String as PropType<ButtonType>, default: "default" },
  /** 按钮尺寸,default,medium,mini */
  size: { type: String as PropType<ButtonSize>, default: "default" },
  /** 按钮形状,circle(两边为半圆),square(带圆角) */
  shape: { type: String as PropType<Shape>, default: "square" },
  /** 按钮是否镂空 */
  plain: { type: Boolean, default: false },
  /** 是否禁止状态 */
  disabled: { type: Boolean, default: false },
  /** 是否加载中 */
  loading: { type: Boolean, default: false },
};

export type ButtonProps = ExtractPropTypes<typeof ButtonProps>;

全局导出:

// uview-pro/types/index.d.ts
// uview-pro 模块类型声明
declare module "uview-pro" {
  // 导出安装函数
  export function install(): void;
  // 导出 utils 工具类型
  export interface Utils {
    // 所有工具类型
  }
}

// 全局类型扩展
declare global {
  interface Uni {
    u: import("uview-pro").Utils;
  }
}

七、总结

通过本篇文章,相信你已经了解了 uView Pro 新版本对 Volar 和 TypeScript 类型提示的全面支持。只需按照文中指引进行简单配置,即可在开发中获得全局组件类型推断、属性校验、事件提示等智能能力,提升开发效率和代码质量。

未来会持续完善类型声明,覆盖更多细节和边界场景,适配更多 IDE 和类型工具。

欢迎大家升级体验,积极反馈问题与建议,共同推动 uView Pro 生态持续完善!

uView Pro 开源地址:

如果 uView Pro 对您有帮助,欢迎给个 Star 支持一下!如果您有任何问题或建议,欢迎在 Issue 区留言或入群交流!

使用 Electron 在 5 分钟内创建一个桌面的 React 应用

作者 Jimmy
2025年8月25日 11:34

原文链接 Create a Desktop React App in Under 5 Minutes with Electron - 作者 Mendoza

当我们使用 React 来构建应用的时候,有时创建一个在桌面被使用的界面更加有意义,特别是它不需要连接到网络上时。我遇到过着这种场景 - 当我想创建一个应用来管理本地文件的时候。我使用 Python 中的 Tkinter,但是,我觉得我可以通过 React 来构建运行更加快速,更加好看的与界面。这也是我开始学习 Electron 的契机。

ElectronGitHub 创造出来的,为了构建它们自己的 Atom 文本编辑器,而现在,已经在流行的软件,比如 VSCodeDiscord 中使用。通过将 ReactElectron 整合使用,就能够快速地开发跨平台的桌面应用。Electron 提供了特定于桌面的额外功能,比如系统级别的应用通知和托盘图标,但是,在本教程中,我将会介绍如何运行 ReactElectron 应用的基础知识。

首先,我们需要创建一个新的 React 应用。我个人喜欢使用 Vite 来创建,这也是我本教程中使用到的脚手架工具,但是,我们使用 create-react-app 异曲同工。我也将使用 Typescript 来配置我的 Vite 应用。

npm create vite@latest <electron-app-name>

然后,进入新创建的应用所在的目录,然后安装 Electron 依赖。

npm install electron --save-dev

接着,我们可以在项目的根目录下创建一个名为 electron.ts 的文件,然后在文件中添加下面的代码:

const { app, BrowserWindow } = require("electron");

function createWindow() {
  // 创建浏览器窗口
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
      enableRemoteModule: true
    }
  });
  
  // 加载 React 应用程序
  win.loadURL("http://localhost:5173");
  
  // 打开调试面板
  win.webContents.openDevTools();
}

// 当应用已经准备好了,创建窗口
app.whenReady().then(createWindow);

// 当所有的窗口到被关闭了,退出 Electron 应用
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

app.on("activate", () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow()
  }
});

现在,我们需要更新根目录下的 package.json 文件。我们添加的内容到 script 脚本对象中:

"start": "react-scripts start",
"electron": "electron ."

然后,我们需要给 Electron 应用一个入口,所以,我们添加一个 main 属性到文件中,值为 electron.ts文件,表明是它就是入口点:

"main": "electron.ts",

最后的 package.json 文件如下所示:

{
  "name": "electron-app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "main": "electron.ts",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "start": "react-scripts start",
    "electron": "electron ."
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@type/node": "^18.15.11",
    "@type/react": "^18.0.28",
    "@type/react-dom": "^18.0.11",
    "@vitejs/plugin-react": "^3.1.0",
    "electron": "^23.2.1",
    "typescript": "^4.9.3",
    "vite": "^4.2.0"
  }
}

现在,通过运行下面的命令行来开启我们的 React 应用。npm run dev 将会开启 React 前端界面,然后 npm run electron 将会开启 Electron 应用并打开在 electron.ts 文件中预设的地址 http://localhost:5173

npm run dev
npm run electron

result.webp

Electron app running on desktop

现在,我们可以用 React 开发我们自己的跨平台桌面应用程序。你可以通过 here 来获取本教程的源代码。我最近专注于使用 React 构建更好的用户界面,你可以到我的 GitHub  中查看。

感谢阅读!

LangChain.js 完全开发手册(二)Prompt Engineering 与模板系统深度实践

作者 鲫小鱼
2025年8月25日 11:26

第2章:Prompt Engineering 与模板系统深度实践

前言

大家好,我是鲫小鱼。是一名不写前端代码的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!

🎯 本章学习目标

  • 掌握 Prompt Engineering 的核心原则与方法论(指令式、少样本、思维链、反思等)
  • 熟练使用 LangChain.js 的 Prompt 模板系统(PromptTemplateChatPromptTemplateFewShotPromptTemplate 等)
  • 能够将 Prompt 与 RunnableCallbackMemoryOutputParser 组合为稳定的可复用链路
  • 学会结构化输出(JSON/Zod Schema)与健壮的错误处理策略
  • 建立 Prompt 评测与 A/B 测试工作流,掌握迭代优化方法
  • 通过两个实战项目,完成从「问题 → 设计 → 编码 → 评测 → 上线」的闭环

📖 理论基础:Prompt Engineering 核心理念(约 30%)

2.1 为什么需要 Prompt Engineering

  • 大模型具备强泛化能力,但输出稳定性受上下文、提示措辞、约束条件影响极大
  • Prompt 的质量决定了结果的可靠性、可控性与成本(token)
  • 工程化的 Prompt 可复用、可测试、可监控、可演进

2.2 基本类型与策略

  • 指令式(Instruction):清晰角色、任务、约束、步骤
  • 少样本(Few-shot):示例驱动,降低歧义,提高风格一致性
  • 思维链(CoT):显式“先思考后作答”,提升推理质量
  • 反思(Reflexion):要求模型自检与修正,降低幻觉率
  • 分而治之(Decompose):复杂任务拆解为子任务流水线
  • 工具化(Tool-Use):与外部工具/检索融合,提升事实性

2.3 好 Prompt 的 4 要素(RICE)

  • Role(角色定位):模型扮演谁(专家/审校/产品经理)
  • Instruction(任务指令):做什么、产出什么格式
  • Context(上下文):必要背景、约束、风格、领域词汇
  • Example(示例):正反示例、边界案例

2.4 可测试与可维护

  • 模板化:变量化可控输入,便于不同调用场景复用
  • 结构化输出:避免纯自然语言,降低解析成本
  • 评测指标:正确率、可读性、一致性、引用率、成本/时延
  • 版本迭代:Prompt 版本号、Changelog、灰度策略

🧩 LangChain.js Prompt 模板体系(约 15%)

2.5 常用类与能力

  • PromptTemplate:文本模板 → 可注入变量
  • ChatPromptTemplate:消息式模板(system/human/ai 等)
  • FewShotPromptTemplate:少样本示例自动拼接
  • PipelinePromptTemplate:子模板管道化组合
  • MessagesPlaceholder:与 Memory 协作,注入历史对话
  • OutputParser:搭配 JSON/Zod 解析为结构化对象

2.6 模板设计准则

  • 单一职责:每个模板聚焦一个目标
  • 边界清晰:说明输入变量、约束、输出格式
  • 可观测性:为评测与追踪预留标记(如 version、taskId)

💻 基础代码实践(约 20%)

2.7 指令式 + 变量注入

// 文件:src/ch02/basic-instruction.ts
import { PromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";
import * as dotenv from "dotenv";
dotenv.config();

const prompt = PromptTemplate.fromTemplate(`
你是一个{role}。请用{style}风格解答:
问题:{question}
要求:
- 使用分点说明
- 控制在{maxTokens}字以内
- 若不确定,请直接说“我需要更多上下文”
`);

const model = new ChatOpenAI({ modelName: "gpt-3.5-turbo", temperature: 0.5 });
const chain = prompt.pipe(model).pipe(new StringOutputParser());

export async function run() {
  const result = await chain.invoke({
    role: "Web 性能专家",
    style: "简洁务实",
    question: "如何优化首屏渲染?",
    maxTokens: 200,
  });
  console.log(result);
}

if (require.main === module) {
  run();
}

2.8 ChatPromptTemplate + 多消息角色

// 文件:src/ch02/chat-prompt.ts
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";

const chatPrompt = ChatPromptTemplate.fromMessages([
  ["system", "你是资深前端教练,善于用类比解释复杂概念。"],
  ["human", "请用类比解释 {topic},并提供一个代码示例。"],
]);

const model = new ChatOpenAI({ modelName: "gpt-3.5-turbo" });
const chain = chatPrompt.pipe(model).pipe(new StringOutputParser());

export async function run() {
  const text = await chain.invoke({ topic: "虚拟 DOM" });
  console.log(text);
}

if (require.main === module) { run(); }

2.9 FewShotPromptTemplate(少样本)

// 文件:src/ch02/few-shot.ts
import { FewShotPromptTemplate, PromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";

const examplePrompt = PromptTemplate.fromTemplate(
  "用户:{input}\n分类:{label}"
);

const examples = [
  { input: "页面加载很慢", label: "性能问题" },
  { input: "按钮点击没反应", label: "交互缺陷" },
  { input: "接口经常 500", label: "后端故障" },
];

const fewShot = new FewShotPromptTemplate({
  examples,
  examplePrompt,
  prefix: "请根据用户诉求给出标签(性能问题/交互缺陷/后端故障):",
  suffix: "用户:{input}\n分类:",
  inputVariables: ["input"],
});

const chain = fewShot.pipe(new ChatOpenAI()).pipe(new StringOutputParser());

export async function run() {
  const out = await chain.invoke({ input: "首屏白屏 3 秒" });
  console.log(out);
}

if (require.main === module) { run(); }

2.10 PipelinePromptTemplate(管道模板)

// 文件:src/ch02/pipeline.ts
import { PromptTemplate, PipelinePromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";

const partA = PromptTemplate.fromTemplate(
  "请将主题扩写为 3 个小标题:{topic}"
);

const partB = PromptTemplate.fromTemplate(
  "基于小标题生成提纲,风格:{style}\n小标题:{headings}"
);

const pipeline = new PipelinePromptTemplate({
  finalPrompt: partB,
  pipelinePrompts: [
    { name: "headings", prompt: partA },
  ],
});

const chain = pipeline.pipe(new ChatOpenAI()).pipe(new StringOutputParser());

export async function run() {
  const result = await chain.invoke({ topic: "前端监控系统", style: "专业简练" });
  console.log(result);
}

if (require.main === module) { run(); }

🧱 结构化输出与 OutputParser(约 10%)

2.11 JSON 输出与严格约束

// 文件:src/ch02/json-output.ts
import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { JsonOutputParser } from "@langchain/core/output_parsers";

type Plan = { steps: { title: string; details: string }[] };

const prompt = PromptTemplate.fromTemplate(`
你是项目规划助手。请输出严格的 JSON:
{
  "steps": [
    { "title": string, "details": string }
  ]
}
主题:{topic}
`);

const model = new ChatOpenAI({ temperature: 0 });
const parser = new JsonOutputParser<Plan>();
const chain = prompt.pipe(model).pipe(parser);

export async function run() {
  const result = await chain.invoke({ topic: "前端监控平台搭建" });
  console.log(result.steps.map(s => `- ${s.title}`).join("\n"));
}

if (require.main === module) { run(); }

2.12 Zod Schema 强类型解析

// 文件:src/ch02/zod-output.ts
import { z } from "zod";
import { StructuredOutputParser } from "@langchain/core/output_parsers";
import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";

const schema = z.object({
  title: z.string(),
  tags: z.array(z.string()).max(5),
  estimateHours: z.number().min(1).max(80),
});

const parser = StructuredOutputParser.fromZodSchema(schema);

const prompt = PromptTemplate.fromTemplate(`
基于需求生成任务卡片:
需求:{requirement}
请严格输出:
{format_instructions}
`);

const chain = prompt.pipe(new ChatOpenAI({ temperature: 0 })).pipe(parser);

export async function run() {
  const out = await chain.invoke({
    requirement: "实现文章阅读进度统计与收藏功能",
    format_instructions: parser.getFormatInstructions(),
  });
  console.log(out);
}

if (require.main === module) { run(); }

🔗 与 Runnable、Memory、Callback 协作(约 10%)

2.13 Runnable 组合与复用

// 文件:src/ch02/runnable-compose.ts
import { RunnableLambda, RunnableSequence } from "@langchain/core/runnables";
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";

const normalize = new RunnableLambda((input: { q: string }) => ({
  q: input.q.trim().slice(0, 300),
}));

const prompt = ChatPromptTemplate.fromMessages([
  ["system", "你是严谨的技术作家,输出规范化 Markdown"],
  ["human", "请回答:{q}"],
]);

const chain = RunnableSequence.from([
  normalize,
  prompt,
  new ChatOpenAI({ temperature: 0.2 }),
  new StringOutputParser(),
]);

export async function run() {
  const md = await chain.invoke({ q: "  讲讲CSR/SSR/SSG 区别  " });
  console.log(md);
}

if (require.main === module) { run(); }

2.14 Memory 注入历史对话(滑动窗口)

// 文件:src/ch02/memory-window.ts
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { ConversationSummaryMemory } from "langchain/memory"; // 或 @langchain/community 中的记忆实现
import { RunnableSequence } from "@langchain/core/runnables";

const prompt = ChatPromptTemplate.fromMessages([
  ["system", "你是持续对话的技术顾问,回答要简洁并引用上下文"],
  new MessagesPlaceholder("history"),
  ["human", "{input}"],
]);

const model = new ChatOpenAI({ temperature: 0 });
const memory = new ConversationSummaryMemory({ llm: model as any, memoryKey: "history" });

const chain = RunnableSequence.from([
  async (input: { input: string }) => ({ ...input, history: await memory.loadMemoryVariables({}) }),
  prompt,
  model,
  async (output) => { await memory.saveContext({}, { output }); return output; },
]);

export async function chatOnce(text: string) {
  const res = await chain.invoke({ input: text });
  console.log(res);
}

if (require.main === module) {
  (async () => {
    await chatOnce("我们刚才讨论了哪些优化点?");
    await chatOnce("继续说说 CSS 层面的优化。");
  })();
}

2.15 Callback:日志与流式观测

// 文件:src/ch02/callbacks.ts
import { ChatOpenAI } from "@langchain/openai";
import { ConsoleCallbackHandler } from "@langchain/core/callbacks/console";
import { ChatPromptTemplate } from "@langchain/core/prompts";

const prompt = ChatPromptTemplate.fromMessages([
  ["system", "你是输出严格 JSON 的助手"],
  ["human", "给出 3 个性能优化建议,输出 JSON 数组。"],
]);

const model = new ChatOpenAI({
  modelName: "gpt-3.5-turbo",
  callbacks: [new ConsoleCallbackHandler()],
  verbose: true,
});

export async function run() {
  const res = await prompt.pipe(model).invoke({});
  console.log(res.content);
}

if (require.main === module) { run(); }

🧪 Prompt 评测与 A/B 测试(约 5%)

2.16 评测清单与指标

  • 准确性(是否回答正题)
  • 可读性(结构、格式、术语友好度)
  • 一致性(相同输入的稳定输出)
  • 事实性(是否引用可信来源,RAG 结合时引用率)
  • 成本与时延(token 使用、响应时间)

2.17 LangSmith 与回归样本集

  • 建立固定问题集(golden set),覆盖主流程与边界
  • 为每次 Prompt 版本变更做回归评测
  • 收集线上真实问题,持续补充样本

2.18 A/B 实战样例(伪代码)

// A/B 两套 Prompt 模板,线上分流 20%/80%
// 记录满意度、转化率、人工标注得分

🚀 实战项目一:FAQ RAG Chat(Prompt 为核心)(约 15%)

2.19 目标

  • 为产品 FAQ/文档提供问答;要求:结构化答案、来源引用、低幻觉
  • 用 Prompt 规范化回答格式;与向量检索(Vector/RAG)配合

2.20 项目结构

src/
  ch02/
    rag-faq/
      ingest.ts         # 文档加载与向量化
      retriever.ts      # 检索器
      prompt.ts         # 回答模板(含引用)
      answer.ts         # 组合链路
      server.ts         # API/CLI 入口

2.21 关键代码(节选)

// 文件:src/ch02/rag-faq/prompt.ts
import { ChatPromptTemplate } from "@langchain/core/prompts";

export const answerPrompt = ChatPromptTemplate.fromMessages([
  ["system", `你是严谨的 FAQ 智能助手。请基于“检索到的片段”回答,若无答案请说不知道。
必须输出以下 JSON:
{
  "answer": string,
  "citations": [{"source": string, "chunkId": string}],
  "confidence": number // 0-1
}`],
  ["human", `用户问题:{question}
检索片段:\n{chunks}\n请回答。`],
]);
// 文件:src/ch02/rag-faq/answer.ts
import { RunnableSequence } from "@langchain/core/runnables";
import { answerPrompt } from "./prompt";
import { ChatOpenAI } from "@langchain/openai";
import { JsonOutputParser } from "@langchain/core/output_parsers";

type QA = { answer: string; citations: { source: string; chunkId: string }[]; confidence: number };

export function buildQAChain(retriever: (q: string) => Promise<string>) {
  const model = new ChatOpenAI({ temperature: 0 });
  const parser = new JsonOutputParser<QA>();
  return RunnableSequence.from([
    async (input: { question: string }) => ({
      question: input.question,
      chunks: await retriever(input.question),
    }),
    answerPrompt,
    model,
    parser,
  ]);
}
// 文件:src/ch02/rag-faq/retriever.ts(示意)
// 实际可用 Chroma/Pinecone 等向量库
export async function simpleRetriever(q: string): Promise<string> {
  return `# chunk-01 来自 docs/intro.md\nLangChain.js 是构建 LLM 应用的框架...`;
}

2.22 Prompt 要点

  • 严格 JSON 输出,便于前端渲染
  • 含“若无答案请直说”的防幻觉护栏
  • 引用列表(citations)强制结构
  • 引入 confidence 便于排序/过滤

2.23 运行与评测

  • 建立 30+ 常见问题作为 gold set
  • A/B 不同语气/结构提示,观察答非所问率
  • 记录引用命中率与人工标注分

🛠️ 实战项目二:Prompt 驱动的「需求 → 任务卡片」生成器(约 15%)

2.24 目标

  • 输入自然语言需求,自动生成结构化任务卡(标题、标签、预估工时、验收标准)
  • 满足结构化输出、可复核、可回填的工程要求

2.25 项目结构

src/
  ch02/
    tasks/
      schema.ts
      prompt.ts
      chain.ts
      eval.ts
      cli.ts

2.26 核心代码(节选)

// 文件:src/ch02/tasks/schema.ts
import { z } from "zod";
export const TaskSchema = z.object({
  title: z.string().min(6),
  tags: z.array(z.string()).max(5),
  estimateHours: z.number().min(1).max(80),
  acceptance: z.array(z.string()).min(1),
});
export type Task = z.infer<typeof TaskSchema>;
// 文件:src/ch02/tasks/prompt.ts
import { PromptTemplate } from "@langchain/core/prompts";
export const taskPrompt = PromptTemplate.fromTemplate(`
请把下面需求转成任务卡片(严格 JSON):
需求:{requirement}
返回:
{
  "title": string,
  "tags": string[],
  "estimateHours": number,
  "acceptance": string[]
}
`);
// 文件:src/ch02/tasks/chain.ts
import { ChatOpenAI } from "@langchain/openai";
import { StructuredOutputParser } from "@langchain/core/output_parsers";
import { TaskSchema } from "./schema";
import { taskPrompt } from "./prompt";

const parser = StructuredOutputParser.fromZodSchema(TaskSchema);
const model = new ChatOpenAI({ temperature: 0 });

export async function generateTask(requirement: string) {
  const res = await taskPrompt
    .pipe(model)
    .pipe(parser)
    .invoke({ requirement });
  return res;
}
// 文件:src/ch02/tasks/eval.ts  (简易评测)
import { generateTask } from "./chain";

const cases = [
  "为博客增加全文搜索,支持标签过滤和高亮",
  "新增图片上传,自动压缩并生成 WebP",
];

(async () => {
  for (const c of cases) {
    const out = await generateTask(c);
    const ok = !!out.title && out.estimateHours > 0 && out.acceptance.length > 0;
    console.log("case:", c, ok ? "✅" : "❌", out);
  }
})();

2.27 质量要点

  • 严格 Schema 校验,异常直接可见
  • 失败样例回收成回归集,持续改进 Prompt
  • 允许少量温度,保证多样性但不过度发散

⚙️ 性能、成本与健壮性(约 5%)

2.28 优化建议

  • 模板复用与缓存:避免重复渲染模板与不必要的调用
  • 限制输出长度与格式:降低 token 使用与解析成本
  • 超时/重试/指数退避:应对网络抖动与临时错误
  • 失败降级:为空时返回兜底文案或引导收集更多上下文

2.29 回退策略(Guardrails)

  • 结构化输出失败 → 自动重试 + 降级纯文本 + 错误上报
  • 检索为空 → 提示继续提问或缩小范围
  • 敏感/越权问题 → 明确拒答并给出替代建议

🔍 与前端/产品工程协作要点(约 5%)

2.30 前端集成

  • 流式输出:打字机效果、取消/重试按钮
  • 结构化渲染:基于 JSON 直接渲染卡片/表格/引用
  • 错误兜底:可视化错误提示与重试引导

2.31 产品落地

  • 指标看板:满意度、人工接管率、拒答率、成本
  • 版本切换:Prompt 版本灰度与快速回滚
  • 合规安全:敏感词治理与数据最小化

📚 资源与延伸

  • LangChain.js 文档(JS):https://js.langchain.com/
  • LangGraph:https://langchain-ai.github.io/langgraph/
  • LangSmith:https://docs.smith.langchain.com/
  • 提示工程最佳实践合集:https://learnprompting.org/
  • OpenAI 指南:https://platform.openai.com/docs/guides/prompt-engineering

📦 附:示例项目最小可运行清单

mkdir -p src/ch02 && cd $_
# 将上述 *.ts 文件放入对应目录后:
npm i @langchain/core @langchain/openai @langchain/community zod dotenv
npm i -D tsx typescript @types/node
echo '{
  "compilerOptions": {"target":"ES2020","module":"commonjs","esModuleInterop":true,"strict":true},
  "include": ["src/**/*"]
}' > tsconfig.json
// package.json 片段
{
  "scripts": {
    "ch02:basic": "tsx src/ch02/basic-instruction.ts",
    "ch02:chat": "tsx src/ch02/chat-prompt.ts",
    "ch02:few": "tsx src/ch02/few-shot.ts",
    "ch02:pipe": "tsx src/ch02/pipeline.ts",
    "ch02:json": "tsx src/ch02/json-output.ts",
    "ch02:zod": "tsx src/ch02/zod-output.ts"
  }
}

✅ 本章小结

  • 系统化掌握了 Prompt 的核心方法与工程化落地
  • 熟练运用 LangChain.js 模板体系与结构化输出
  • 学会将 Prompt 与 Runnable/Memory/Callback 组合
  • 建立 Prompt 评测与 A/B 测试的迭代闭环
  • 完成两个实战项目,从 0 到 1 走通产品化路径

🎯 下章预告

下一章《Memory 系统与对话状态管理》中,我们将深入:

  • 短期/长期记忆策略的权衡
  • 滑动窗口与摘要记忆的组合
  • 与向量记忆(VectorStore)协同,打造可检索的对话系统
  • 记忆的一致性、成本与隐私安全

最后感谢阅读!欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!!

前端监控实战:从性能指标到用户行为,我是如何搭建监控体系的

作者 ErpanOmer
2025年8月25日 11:17

image.png

还在当一线开发的时候,我最怕半夜接到电话,说:线上出问题了!!!。

那时候我们对线上环境几乎是两眼一抹黑。一个功能发布后,它在线上跑得快不快、有没有报错、用户到底喜不喜欢用,我们一概不知。出了问题,只能靠用户反馈和后端模糊的日志,排查效率极低,过程也极其痛苦。

当了组长后,我下定决心,必须改变这种被动救火的局面,能实时地看到线上应用的健康状况。

所以,我花了几个月的时间,搭建了一套前端监控体系。这篇文章,就是我们团队这套监控体系的实战总结。它主要分为三大块,回答三个核心问题:

  1. 性能监控:我们的网站快吗?
  2. 异常监控:我们的网站稳定吗?
  3. 行为监控:用户在我们网站上做了什么?

性能监控

监控什么?

性能指标有很多,但我们初期应该聚焦在 核心Web指标(Core Web Vitals) 上,这是Google官方定义的、直接关系到用户体验的三个核心指标。

  • LCP (Largest Contentful Paint):最大内容绘制。衡量加载性能。简单说,就是用户看到页面主要内容花了多久。
  • INP (Interaction to Next Paint):下次绘制前交互。衡量交互性能。这是2024年取代FID的新指标,衡量用户点击、输入等操作后,页面给出视觉反馈的速度。
  • CLS (Cumulative Layout Shift):累积布局偏移。衡量视觉稳定性。比如页面加载时,图片突然出现导致下方按钮被挤下去,就是一次不好的CLS。

怎么实现?

我们没有重复造轮子,直接选用了Google官方的小型库web-vitals来采集指标。

  • 第一步:安装
    pnpm add web-vitals
    
  • 第二步:在你的应用入口处(如main.js)进行采集
    import { onLCP, onINP, onCLS } from 'web-vitals';
    
    // 封装一个上报函数
    function sendToAnalytics(metric) {
      // 这里是你把数据发送到自己后端服务的逻辑
      // 为了保证数据在页面关闭前也能成功发送,强烈建议使用 navigator.sendBeacon
      const body = JSON.stringify({ [metric.name]: metric.value });
      const url = "/your/analytics/endpoint";
    
      if (navigator.sendBeacon) {
        navigator.sendBeacon(url, body);
      } else {
        fetch(url, { body, method: 'POST', keepalive: true });
      }
    }
    
    // 采集并上报
    onLCP(sendToAnalytics);
    onINP(sendToAnalytics);
    onCLS(sendToAnalytics);
    
  • 第三步:后端接收与展示 你需要一个简单的后端服务来接收这些数据,并存入数据库。然后通过可视化工具(如Grafana)来展示性能大盘。

image.png


异常监控

监控什么?

  • JS运行时错误:代码里的逻辑错误,比如cannot read property 'xxx' of undefined
  • Promise异步错误Promise中未被catchreject
  • 静态资源加载错误:JS、CSS、图片等资源加载失败。
  • 白屏错误:这是一个比较难定义,但非常致命的错误。

怎么实现?

异常监控的体系非常复杂,涉及到错误捕获、堆栈解析、SourceMap还原、上报、聚合、告警等一整套流程。这块我们坚决不自己造轮子,直接选择了成熟的开源方案:Sentry

  • 第一步:接入Sentry SDK

    pnpm add @sentry/browser @sentry/tracing
    
  • 第二步:在应用入口处初始化

    import * as Sentry from "@sentry/browser";
    import { BrowserTracing } from "@sentry/tracing";
    
    Sentry.init({
      dsn: "你在Sentry官网上获取的DSN地址",
      integrations: [new BrowserTracing()],
      
      // 我们只关心生产环境的错误
      environment: 'production', 
      
      // 设置性能监控的采样率
      tracesSampleRate: 0.1, // 10%的页面会采集性能数据
    });
    

    只需要这几行代码,Sentry就会自动帮我们监听全局的JS错误、Promise异常和资源加载异常。

  • 第三步(最重要的一步):上传Source Map

    Source Map是异常监控的灵魂。 没有它,你Sentry上看到的错误堆栈,就是一堆经过压缩混淆的、毫无意义的天书。

    我强制要求我们团队的CI/CD流程,必须在每次生产构建后,自动将Source Map上传到Sentry。这可以通过Sentry官方的sentry-cli或Webpack/Vite插件来完成。

具体怎么使用网上有不少教程,可以参考这位博主的👉 bug 追踪系统 Sentry (4) -- 关联 sourceMap


行为监控

性能和稳定性保证了我们的应用 能不能用,而行为监控,则是为了搞清楚应用 好不好用

监控什么?

  • 基础流量:PV(页面浏览量)、UV(独立访客数)、用户停留时长等。
  • 用户转化漏斗:比如“访问商品页 -> 点击加入购物车 -> 进入结算页 -> 支付成功”这一整个流程中,每一步的用户流失率是多少。
  • 功能使用率:某个按钮的点击次数、某个功能的渗透率等。

2. 怎么实现?

这块的选择很多,从免费的Google Analytics,到更专业的Mixpanel、Amplitude。

但无论用什么工具,作为组长,我要求团队在代码层面,必须遵守一个原则:封装一个统一的埋点(tracking)函数

  • 第一步:封装一个tracker.js
    // src/utils/tracker.js
    
    /**
     * 统一的埋点上报函数
     * @param {string} eventName 事件名称
     * @param {object} properties 事件属性
     */
    export function trackEvent(eventName, properties = {}) {
      // 可以在开发环境下打印,方便调试
      if (process.env.NODE_ENV === 'development') {
        console.log(`[Track Event]: ${eventName}`, properties);
      }
    
      // 在这里,你可以接入任何第三方或自研的埋点SDK
      // 比如 Google Analytics
      // window.gtag?.('event', eventName, properties);
    
      // 比如 Mixpanel
      // window.mixpanel?.track(eventName, properties);
    }
    
  • 第二步:在业务组件中调用
    import { trackEvent } from '@/utils/tracker';
    
    function AddToCartButton({ productId }) {
      const handleClick = () => {
        // 业务逻辑...
    
        // 调用统一的埋点函数
        trackEvent('click_add_to_cart', { 
          productId,
          from: 'product_detail_page' 
        });
      };
      return <button onClick={handleClick}>加入购物车</button>;
    }
    

为什么要封装? 因为这样能让我们的业务代码,和具体的埋点SDK实现解耦。未来如果我们想从Google Analytics切换到Mixpanel,我们只需要修改tracker.js这一个文件,而不需要去改动散落在项目里成百上千个业务组件。


搭建监控体系,终点不是收集数据和看报表,而是驱动行动

搭建这套体系,让我们的团队工作模式,真正从靠感觉猜,升级到了靠数据 处理特定问题。虽然过程很麻烦,但每一分投入,都非常值得。

分享完毕 谢谢大家

从0到1:手把手带你开发第一个Chrome插件

作者 金金金__
2025年8月25日 11:05

金金金上线!
话不多,只讲你能听懂的前端知识

image.png

前言

谁不想拥有一个属于自己的浏览器插件呢~

开始

  1. 创建一个新目录
  2. 创建manifest.json文件,添加以下内容:
    {
      "name": "Hello Extensions",
      "description": "Base Level Extension",
      "version": "1.0",
      "manifest_version": 3,
      "action": {
        "default_popup": "hello.html",
        "default_icon": "hello_extensions.png"
      }
    }
    
  3. 下载个icon图片并命名为hello_extensions.png,需要和default_icon一致
  4. 创建一个文件,hello.html(弹出式窗口,就是点击了扩展会弹出一个小框显示信息),添加以下内容:
    <html>
     <body>
       <h1>Hello Extensions</h1>
     </body>
    </html>
    

加载未打包的扩展程序

如需在开发者模式下加载未封装的扩展程序,请执行以下操作:

  1. 在新标签页中输入 chrome://extensions 即可前往“扩展程序”页面。(根据设计,chrome:// 网址不可链接。)

    • 或者,点击“扩展程序”菜单拼图按钮,然后选择菜单底部的管理扩展程序
    • 或者,点击 Chrome 菜单,将鼠标悬停在更多工具上,然后选择扩展程序
  2. 点击开发者模式旁边的切换开关,即可启用开发者模式。

  3. 点击加载已解压缩的文件按钮,然后选择扩展程序目录。(可能没那么快响应,记得稍等一会)

    image.png

这样就代表成功了

固定扩展程序

默认情况下,当您在本地加载扩展程序时,它会显示在扩展程序菜单中。将扩展程序固定到工具栏,以便在开发过程中快速访问扩展程序。

image.png

点击扩展程序的操作图标(工具栏图标);您应该会看到一个弹出式窗口。

企业微信截图_17560895667935.png

重新加载扩展程序

  • 有时候会有修改内容的需求,此时改完后 就需要重新加载扩展程序以看到最新的内容

  • 修改manifest.json,将扩展程序的名称更改为 Hello Extensions of the world!

    企业微信截图_17560897159213.png

保存文件后,您还必须刷新扩展程序,才能在浏览器中看到此更改。前往“附加信息”页面,然后点击开启/关闭切换开关旁边的刷新图标:

企业微信截图_17560898118564.png

验证

企业微信截图_17560898907529.png

何时应该重新加载扩展程序呢?

  • 下表显示了哪些组件需要重新加载才能看到更改:

    扩展程序组件 需要重新加载扩展程序
    清单
    Service Worker
    内容脚本 是(包括托管网页)
    弹出式窗口
    “选项”页面
    其他扩展程序 HTML 网页

查找控制台日志和错误

控制台日志

  • 在开发期间,您可以通过访问浏览器控制台日志来调试代码。在本例中,我们将找到弹出式窗口的日志。首先,将脚本标记添加到 hello.html
    <html>
      <body>
        <h1>Hello Extensions</h1>
        <script src="popup.js"></script>
      </body>
    </html>
    
  • 创建一个 popup.js 文件,并添加以下代码:
    console.log("This is a popup!")
    

在控制台中查看系统记录的这条消息,请执行以下操作:

  1. 打开弹出式窗口。

  2. 右键点击弹出式窗口。

  3. 选择检查

    image.png

  4. 在 DevTools 中,前往控制台面板

    image.png

错误日志

  • 现在,来破坏一下扩展程序。为此,移除 popup.js 中的闭引号:

    image.png

  • 前往“扩展程序”页面,然后打开弹出式窗口。系统随即会显示一个错误按钮。

    image.png

  • 点击错误按钮,详细了解相应错误:

image.png

如需详细了解如何调试服务工件、选项页面和内容脚本,请参阅调试扩展程序


可以通过多种方式构建扩展程序项目;不过,唯一的前提是将 manifest.json 文件放在扩展程序的根目录中(之后出如何构建扩展程序相关文章~)


编写有误还请各位指正,万分感谢

【AI解读源码系列】ant design mobile——Avatar头像

作者 sorryhc
2025年8月25日 11:01

前言

笔者目前业务主要围绕ant design mobile组件库来交付H5前端工作。

故此出此专栏来解读每一个组件是如何实现的。

本文基于AI来解读image组件。

文档链接:

mobile.ant.design/zh/componen…

源码:

github.com/ant-design/…

设计prompt

解读antd-mobile avatar组件,结构化思维输出整个组件实现流程,并且解读fallback.tsx是如何实现组件兜底的,按步骤提供关键代码解读

// 附组件代码

实现步骤

第一步:属性设计与合并

  • Avatar 支持图片地址、兜底图、展示模式、事件等各种属性,并自动合并默认值。
const defaultProps = {
  fallback: <Fallback />, // 默认兜底头像
  fit: 'cover',           // 默认居中裁剪
};
const props = mergeProps(defaultProps, p); // 合并props

第二步:图片地址预处理

  • 去除 src 的空白字符,为空/无效用 undefined,保证 Image 组件兜底逻辑触发。
const mergedSrc = props.src?.trim() || undefined;

第三步:调用 Image 组件渲染头像(最核心)

  • withNativeProps 把所有原生属性增强到最外层 DOM,并传递给 <Image />
  • fallback 和 placeholder 都设置为兜底图,任何载入中/失败情况都一致。
return withNativeProps(
  props,
  <Image
    className={classPrefix}
    src={mergedSrc}
    fallback={props.fallback}
    placeholder={props.fallback}
    alt={props.alt}
    lazy={props.lazy}
    fit={props.fit}
    onClick={props.onClick}
    onError={props.onError}
    onLoad={props.onLoad}
  />
);

第四步:Image 组件内部实现兜底

  • Image 组件在 src 无效、加载失败时会自动渲染 fallback。

第五步:Fallback 组件(svg兜底)实现方式

  • 默认 fallback 是 <Fallback />,实际为一个灰色 SVG 头像(React.memo封装,避免多次渲染)。
  • 只要没图、加载失败、加载中都会显示这个兜底 SVG。
export const Fallback = memo(() => (
  <svg
    className='adm-avatar-fallback'
    width='88px'
    height='88px'
    viewBox='0 0 88 88'
    // ...省略具体SVG
  />
));

第六步:自定义支持

  • 支持自定义 fallback,开发者可以用自己的 logo/图片/图标作为头像兜底

总结

  1. 合并props,保证有默认fallback和fit
  2. 预处理src,避免空图带来异常
  3. 核心渲染Image,placeholder和fallback都传兜底SVG
  4. Image组件自动处理所有加载失败/空src,用fallback兜底
  5. Fallback.tsx里实现具体兜底SVG头像,memo优化渲染

Avatar组件比较简单,核心逻辑都包在Image组件中了,如果想了解Image组件的具体实现可参考:

juejin.cn/post/754057…

以上就是笔者基于AI返回的解读信息稍加了一些补充和修改,结合起来看源码提效真是太多了,对于前端本身就是基于视图所完成编码,因此把组件逻辑层交给AI来解读太适合不过了。

希望对大家有所帮助,共同学习源码。

Clean Code JavaScript小记(二)

作者 Nayana
2025年8月25日 11:01

单一职责原则

单一职责原则是面向对象设计原则,为了降低复杂性,耦合性和提高可维护性。一个类只承担一项职责与功能,承担的功能更多会让可读性降低。

实现单一职责的原则

1.识别和分离职责

识别类中是否存在多个功能。

2.分解类

识别出多功能,下一步分解功能到不同的类中。

// 反例
user类既维护了又用户信息做对用户做了校验。
class User {
  constructor(name, email, password) {
    this.name = name;
    this.email = email;
    this.password = password;
  }
 
  getUserInfo() {
    return { name: this.name, email: this.email };
  }
 
  authenticate(providedPassword) {
    return providedPassword === this.password;
  }
}

//正例
// 分解成2个类  区分用户信息和校验

class UserInfo {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
 
  getUserInfo() {
    return { name: this.name, email: this.email };
  }
}
 
class UserAuthentication {
  constructor(password) {
    this.password = password;
  }
 
  authenticate(providedPassword) {
    return providedPassword === this.password;
  }
}
开/闭原则

代码实体(类,模块,函数等)应该易于扩展,难于修改以适应新的需求

// 反例
class AjaxRequester {
  constructor() {
    // What if we wanted another HTTP Method, like DELETE? We would have to
    // open this file up and modify this and put it in manually.
    this.HTTP_METHODS = ['POST', 'PUT', 'GET'];
  }

  get(url) {
    // ...
  }

}
// 正例:
class AjaxRequester {
  constructor() {
    this.HTTP_METHODS = ['POST', 'PUT', 'GET'];
  }

  get(url) {
    // ...
  }

  addHTTPMethod(method) {
    this.HTTP_METHODS.push(method);
  }
}

实现类的开闭原则也会涉及到几个关键的设计模式和编程实践,如工厂模式,策略模式组合模式。

里氏替换原则

子类可以扩展父类的功能,但不能改变父类原有的功能。 遵循此原则的编码建议:

  • 子类应通过添加新方法或重写父类抽象方法扩展功能,而非修改父类抽象方法。
  • 参数与返回类型兼容:子类重写父类方法时参数应更宽松(支持多种类型),返回值应更严格。
  • 使用抽象类或接口:通过抽象类或接口定义稳定的部分,将可变的部分封装在具体实现中避免破坏原则。
示例:
class Animal {
  constructor () {
        
      }
      makeSound(...animals) {
        for(let animal of animals) {
            if (animal instanceof Animal) {
                console.log(animal.name, animal.makeSound())
            } else {
                console.log('拒绝表演,下次别来了')
            }
        }
    }
    
}

class Dog extends Animal{
  constructor (name) {
        this.name=name
      }
      makeSound() {
        console.log("汪汪");
    }

}

class Cat extends Animal{
  constructor (name) {
        this.name=name
      }
      makeSound() {
        console.log("喵");
    }

}
let Animal =new  Animal()
Animal.makeSound(new Dog('小狗'),new Cat('小喵'));

// 这里Animal.makeSound方法不依赖小狗和小喵的实例还是依赖 Animal实例;;小狗/小喵的 makeSound 方法的实现就是里氏替换。
接口隔离原则

在 JS 中,当一个类需要许多参数设置才能生成一个对象时,或许大多时候不需要设置这么多的参数。此时减少对配置参数数量提高系统的灵活性和可维护性。

接口隔离原则的核心思想

  • 单一职责原则:每个接口应该只包含一个客户端(使用者)使用的方法,这样可以避免客户端依赖于不需要的方法。
  • 高内聚、低耦合:通过将接口拆分成更小的部分,可以减少类之间的耦合,提高系统的内聚性。

// 反例
class MultiFunctionPrinter {
  print() {}
  scan() {}
  fax() {}
  staple() {}
}

// 正例 - 接口隔离
class Printer {
  print() {}
}

class Scanner {
  scan() {}
}

class FaxMachine {
  fax() {}
}

class Stapler {
  staple() {}
}

// 组合功能
class AllInOnePrinter {
  constructor(printer, scanner, faxMachine) {
    this.printer = printer;
    this.scanner = scanner;
    this.faxMachine = faxMachine;
  }

  print() {
    this.printer.print();
  }

  scan() {
    this.scanner.scan();
  }

  fax() {
    this.faxMachine.fax();
  }
}
依赖反转原则

高层模块不应该依赖低层模块 里氏替换实例中 ,Animal.makeSound方法 就是依赖自身的方法。不依赖底层类方法

 示例
 // 抽象的日志记录器接口
class Logger {
    log(message) {
        throw new Error("Abstract method must be implemented");
    }
}
// 具体的控制台日志记录器实现
class ConsoleLogger extends Logger {
    log(message) {
        console.log(message);
    }
}
// 业务逻辑类,依赖于抽象的日志记录器
class BusinessLogic {
    constructor(logger) {
        this.logger = logger;
    }
    doSomething() {
        let result = "Some operation result";
        this.logger.log(result);
    }
}
// 使用控制台日志记录器注入到业务逻辑类
let business = new BusinessLogic(new ConsoleLogger());
business.doSomething();

使用方法链

// 反例
class Car {
  constructor() {
    this.make = 'Honda';
    this.model = 'Accord';
    this.color = 'white';
  }

  setMake(make) {
    this.name = name;
  }

  setModel(model) {
    this.model = model;
  }

  setColor(color) {
    this.color = color;
  }

  save() {
    console.log(this.make, this.model, this.color);
  }
}

let car = new Car();
car.setColor('pink');
car.setMake('Ford');
car.setModel('F-150')
car.save();

//正例
class Car {
  constructor() {
    this.make = 'Honda';
    this.model = 'Accord';
    this.color = 'white';
  }

  setMake(make) {
    this.name = name;
    // NOTE: Returning this for chaining
    return this;
  }

  setModel(model) {
    this.model = model;
    // NOTE: Returning this for chaining
    return this;
  }

  setColor(color) {
    this.color = color;
    // NOTE: Returning this for chaining
    return this;
  }

  save() {
    console.log(this.make, this.model, this.color);
  }
}

let car = new Car()
  .setColor('pink')
  .setMake('Ford')
  .setModel('F-150')
  .save();

处理并发

用promise 替代回调

回调不够整洁可能会造成大量嵌套 ES6内置了promise直接使用,promise的使用提高代码整洁性和可维护性

//反例
require('request').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', function(err, response) {
  if (err) {
    console.error(err);
  }
  else {
    require('fs').writeFile('article.html', response.body, function(err) {
      if (err) {
        console.error(err);
      } else {
        console.log('File written');
      }
    })
  }
})

//正例

require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
  .then(function(response) {
    return require('fs-promise').writeFile('article.html', response);
  })
  .then(function() {
    console.log('File written');
  })
  .catch(function(err) {
    console.error(err);
  })

Async/Await 是较 Promises 更好的选择

promise是相较回调来说是一种更好的选择,ES7中的await和async 更胜于promise

示例
async function getCleanCodeArticle() {
  try {
    var request = await require('request-promise')
    var response = await request.get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin');
    var fileHandle = await require('fs-promise');

    await fileHandle.writeFile('article.html', response);
    console.log('File written');
  } catch(err) {
    console.log(err);
  }
}

错误处理

使用try/catch 捕获错误并且应当对 catch捕获的报错做出相应的处理方案

// 反例:
try {
  functionThatMightThrow();
} catch (error) {
  console.log(error);
}
// 正例
try {
  functionThatMightThrow();
} catch (error) {
  // One option (more noisy than console.log):
  console.error(error);
  // Another option:
  notifyUserOfError(error);
  // Another option:
  reportErrorToService(error);
  // OR do all three!
}

promise的catch捕获的报错做出相应的处理方案 与try/catch 同理

格式化

  • 大小写一致,针对团队制定的统一规则保持风格一致.
  • 调用函数和被调用的函数应该放在相近的位置.

性能治理之页面LongTask优化

2025年8月25日 10:58

前言

在现代 Web 应用中,页面性能直接影响用户体验和业务发展。我们近期对列表页进行了一次全面的性能优化,旨在提升用户访问效率,提升用户满意度。

接下来,将为大家详细分享这次优化的过程、方法和成果。

一、优化目标与用户分析

我们将优化目标聚焦于核心列表页,通过BLM健康平台(内部访问统计平台)筛选出高流量且秒开率低的页面作为重点优化对象。同时,利用日志进行深度剖析,明确优化的用户范围。

  • 初次进入用户:占比18.41%,秒开率接近于0;
  • 二次进入用户:占比81.87%,秒开率27.85%。

基于这一数据,我们确定二次进入用户中的未秒开用户是本次优化的主要目标群体,占比约为58.85%,这一决策基于两个关键考量:

  • 初次进入用户受网络环境和首次加载影响较大,优化空间有限;

  • 二次进入用户占比高且已有部分秒开,优化潜力更大。

二、性能优化尝试

qiankun 架构下某核心列表加载流程简图

在 qiankun 架构下,列表的加载流程具有一定的复杂性。我们针对这一流程,分阶段展开,具体内容如下:

第一阶段:静态资源优化(DLC)

首先从静态资源入手,对相关文件和模块进行调整,具体优化内容及前后指标对比如下:

关键收获:非关键资源的延迟加载或移除可以显著降低TBT(Total Blocking Time 总阻塞时间)。

第二阶段:DLC到FCP

本次优化暂不考虑

第三阶段:主应用FCP到LCP优化

针对主应用的关键渲染路径,重点优化了:

// 修改前:筛选项全部渲染
renderAllFilters() {
  // 渲染所有筛选项(包括隐藏内容)
}

// 修改后:按需渲染
renderVisibleFiltersOnly() {
  // 只渲染可见的筛选项
}

优化效果对比:

第四阶段:长任务分解

核心优化方案

基于以上分析,我们总结出以下优化建议(按收益排序):

  1. 筛选项懒加载:TTI最多减少642ms;
  2. 大数据枚举虚拟化:司机标签TTI减少594ms,城市组件减少179ms;
  3. 接口调用顺序调整:TTI减少355ms;
  4. 表格组件优化:TTI减少430ms;
  5. 缓存逻辑优化:TTI减少170ms;
  6. CSS合包处理:TTI减少60ms;

优化效果验证

通过A/B测试和监控平台数据对比:

  • 优化前(02.12-02.18):秒开率17.15%;
  • 优化后(02.28-03.04):秒开率31.92%;
  • 提升幅度:14.77个百分点;

三、分析方法沉淀

目标设定阶段

  1. 通过BLM健康平台确定需优化页面
  • 高流量页面优先
  • 秒开率低页面优先
  1. 通过性能埋点日志确定可优化范围

    使用 Node 脚本将一段时间日志进行分类分析:是否初次进入 / 是否存存在缓存。

    确定可优化用户范围,若用户范围过小或无法达成,可放弃优化,转向其他页面。

问题定位方法

我们主要使用以下工具进行性能分析:

关键指标解释:

  • FCP(First Contentful Paint):首次内容渲染
  • LCP(Largest Contentful Paint):最大内容渲染
  • TBT(Total Blocking Time):总阻塞时间(FCP到TTI之间超过50ms的任务部分总和)
  • TTI(Time to Interactive):可交互时间

  • Lighthouse初步诊断:获取整体性能评分和优化建议。

  • 任务分析:通过 Performance insights 的 Main 线程火焰图定位长任务 & 检查接口并行性和响应时间。

  • Performance深度分析
  1. 使用 Performance 工具打开待优化页面,根据任务分析中找到的阻塞点函数进行搜索定位。在有 map 文件的环境或本地环境中,通过调用栈信息,找到阻塞或长任务对应的代码块。
    • LCP 后的任务阻塞一般是由接口返回数据渲染导致,这时可以结合 Network 火焰图找对应关系,明确是那个 API;

    • Network 中资源条目的时间包括请求前等待时间以及请求后解析时间,可根据解析时间判断哪个接口数据导致了阻塞;

    • 可在函数执行前添加console.profile(),在函数执行后添加console.profileEnd()语句,定向分析该函数对性能的影响(此方法对异步任务的支持有限)。

// 示例:标记关键函数执行
console.profile('filterRender');
renderFilters();
console.profileEnd('filterRender');

  1. 主要通过以上三步确定阻塞点并进行分析。若想深入分析阻塞点的具体原因,可在本地环境中使用 Performance 进行分析(可获取具体函数调用栈),或使用 Profiles(在 Memory 标签下)进行分析,以确定哪些函数导致了内存占用的波动等问题。

方案验证(whistle)

安装好 whistle 客户端并配置规则,将需要更改的文件代理到本地进行修改;

本文所得数据均以此方式进行测试

确定修改方案的ROI

根据分析以上分析列出可优化点,并对所有优化方案进行评估计算 ROI,优先选择收益高且优化成本低的优化方案进行优化。

结果验证阶段

  • 对优化方案进行优化开发和测试;
  • 上线后通过健康平台和性能日志进行数据指标验证是否达到预期。

四、经验总结与未来建议

基于本次优化经验,我们提炼出以下通用建议:

1.按需加载原则

  • 非首屏内容延迟加载;
  • 大数据枚举使用虚拟滚动;

2.关键渲染路径优化:

  • 关键接口尽早调用(created生命周期);
  • 避免渲染阻塞接口请求;

3.长任务分解:

  • 将超过50ms的任务拆分为小任务;
  • 使用 Web Worker 处理复杂计算;

4.缓存策略:

  • 合理使用内存和 IndexedDB 缓存
  • 避免缓存更新阻塞主线程

5.监控体系:

  • 建立完整的性能监控体系

  • 定期分析用户行为日志

结语

本次优化实践表明,系统化的性能优化需要:

  1. 精准的用户行为分析确定优化方向;
  2. 科学的工具链进行问题定位;
  3. 严谨的ROI评估确定优化优先级;
  4. 完善的监控体系验证优化效果。

通过这四步方法论,不仅提升了特定页面的性能,更建立了一套可复用的前端性能优化体系,为后续其他页面的优化提供了宝贵经验。性能优化是一个持续的过程,我们将不断探索和实践,为用户带来更流畅、高效的使用体验。希望本次分享能为大家在页面性能优化方面提供有益的参考和借鉴。

附:

Lighthouse

Lighthouse 是 Google 开发的一款工具,用于分析网络应用和网页,收集现代性能指标并提供对开发人员最佳实践的意见。为 Lighthouse 提供一个需要审查的网址,它将针对此页面运行一连串的测试,然后生成一个有关页面性能的报告。

页面的性能 Performance 评分,包括:

  • 首次内容绘制(FCP First Contentful Paint);
  • 最大内容渲染时间(LCP largest contentful Paint)衡量感知加载速度;
  • 总阻塞时间 (TBT Total Blocking Time)TBT 衡量的是页面被阻止响应用户输入(例如点击鼠标、点按屏幕或按键盘)的总时长。总和的计算方法是将 First Contentful Paint 和 Time to Interactive 期间所有长任务的阻塞部分相加。任何执行时间超过 50 毫秒的任务都是耗时较长的任务。50 毫秒后的时间是阻塞部分。例如,如果 Lighthouse 检测到一个时长为 70 毫秒的任务,则阻塞部分将为 20 毫秒。
  • 累积布局偏移(CLS Cumulative Layout Shift)衡量视觉稳定性;
  • 速度指标(Speed Index)

Performance

Performance 是 Chrome 提供给我们的开发者工具,用于记录和分析我们的应用在运行时的所有活动。它呈现的数据具有实时性、多维度的特点,可以帮助我们很好地定位性能问题。 使用 Performance 工具时,为了规避其它 Chrome 插件对页面的性能影响,我们最好在无痕模式下打开页面。

  • 控制面板 | 开始记录,停止记录和配置记录期间捕获的信息。
  • 概览面板 | 对页面表现(行为)的一个概述。
    • FPS | 绿色的柱越高, FPS 值也越高,红色则说明可能出现了卡顿。
    • CPU | 表明了哪些事件在消耗 CPU 资源。
    • NET | 蓝色 代表 HTML 文件,黄色 代表 Script 文件,紫色 代表 Stylesheets 文件, 绿色 代表 Media 文件,灰色 代表其他资源。
  • 火焰图面板 | 可视化 CPU 堆栈(stack)信息记录。
    • 从不同的角度分析框选区域 。例如:Network,Frames, Interactions, Main 等。

Performance insights

Performance insights 是 Chrome Chrome DevTools中的自带工具(Chrome102 版本发布),目前还是在Chrome DevTool中启动即可,如下图所示:我们可以模拟cpu,选择4x slowdown,就开始模拟4倍低速 CPU,同理还可以模拟网络应对不同网络的测试需求。 Performance insights 工具最方便的部分是"insights"面板,它位于面板的最右侧。它以垂直时间线的形式按照事件发生的顺序显示事件,如渲染阻塞请求、长任务、布局变化等。点击这些具体事件将导航到"详细信息"选项卡,它给出了关于它的潜在原因和受它影响的元素等的详细信息,在Details中看到影响性能问题的各种因素。想要进一步进行优化,点击该事件就可以查看关于问题的详细描述和具体的优化方案。另外在页面的顶端(绿色框)我们可以方便的看到当前页面DCL,FCP,LCP和TTI这些参数指标

性能监控日志

性能监控日志以 JSON 格式记录了丰富的页面性能数据。我们可以将埋点日志下载到本地,通过 Node 脚本对其进行计算和分析,从而获取所需的数据,如 IndexedDB 缓存平均等待时间、资源加载及解析时间、接口响应后到 TTI 计算结束时间等,也可以通过此方式计算优化某个点的收益等。

开发小结(08.11-08.16)

作者 Ankkaya
2025年8月25日 10:56

本周一直在做 uniapp 的微信小程序项目,因此涉及的问题都是在该场景下出现

scroll-view 高度计算

在涉及下拉刷新和上拉加载的场景,scroll-view 是不错的选择,使用的时候需要传一个固定高度,如何准确计算出滚动区域高度,就很重要

如图,底部粉色区域为滚动区域,要计算滚动区域高度,用页面高度(100vh)减去其他区域(红色,绿色,蓝色)高度即可,还需要注意在滚动区域以外,如果有设置 marginTop,marginBottom,也要减去这个高度

在设计页面的时候,最好区分好每个部分的功能,不仅开发起来方便,也有利于计算高度,最后得出滚动区域高度

this.scrollViewHeight = `calc(100vh - (${statusBarHeight}px + ${navigationBarHeight}px + ${uni.upx2px(290)}px + ${uni.upx2px(44 + 10)}px))`;

红框部分实现的是自定义导航栏:高度为${statusBarHeight}px + ${navigationBarHeight}px

蓝框部分用rpx设置的相对高度,需要用uni.upx2px(290)

蓝框部分设置的44rpx加上蓝框和红框之间margin,同样要用uni.upx2px(44 + 10)转化

这样就能准确得出滚动区域高度,但如果其他区域没有设置高度,如何获取该区域高度呢,请接着看自定义 tabbar

自定义 tabbar

还是上面的页面,我需要实现一个 tabbar,要求当前激活项下显示红色短线,并且切换项目时候,短线有移动动画

先根据需求设置基本样式,.tab-item为选项卡部分,.active-bar为底部红色短线

  <view class="tabs">
    <view v-for="(item, idx) in tabList" :key="item.name" class="tab-item"
      :class="{ active: idx === activeTabIndex }" @click="changeTab(idx)">
      {{ item.name }}
    </view>
    <view class="active-bar" :style="{
          left: activeBarLeftPx + 'px'
        }">
    </view>
  </view>

部分样式

.active-bar {
  position: absolute;
  bottom: -6rpx;
  height: 6rpx;
  border-radius: 30rpx;
  background-color: rgba(255, 43, 88, 1);
  width: 40rpx;
}

难点在于获取当前选项卡的位置,才能正确设置短线的位置,因为在 uni 的小程序平台不支持页面的 ref 属性,这里要用到uni.createSelectorQueryapi

在页面加载完成后,查询默认第一个选项卡位置

mounted() {
  // 初始化时获取第一个tab的位置
  this.$nextTick(() => {
    this.queryTabRect(this.activeTabIndex);
  });
},
  
methods: {
  queryTabRect(idx) {
    uni.createSelectorQuery()
      .in(this)
      .selectAll('.tab-item')
      .boundingClientRect(rects => {
        if (rects && rects[idx]) {
          this.activeBarLeftPx = rects[idx].left + (rects[idx].width - uni.upx2px(40)) / 2; // 相对导航栏左侧
        }
      })
      .exec();
  },
}

this.activeBarLeftPx.active-bar相对父容器的距离,如何计算距离呢,rects[idx].left是当前选项卡距离父容器左边距,(rects[idx].width - uni.upx2px(40)) / 2是选项卡和底部短线在竖直方向居中对齐时,多出来长度的一半,加上这个长度,就能正确计算出底部短线距离父容器左边框的距离

容器高度

回到上面scroll-view 高度计算部分,其他区域高度也可以使用uni.createSelectorQueryapi 获取,结果就是rects[idx].height,需要注意这个高度不包括padding,所以计算高度时候需要加上padding

单行文字省略效果

小程序里有一个排行榜的页面,每条记录可能存在名称或者数值溢出的情况,可以考虑使用 css 实现文字溢出显示省略号的功能,不过需要注意使用条件

text-overflow 用于确定如何提示用户存在隐藏的溢出内容,但需要为容器设置准确宽度

从上图分析,内容区域宽度为蓝色区域的宽度,左边文字部分和右边数字部分可能存在溢出情况,就需要给这两个部分添加溢出样式

  .overflow-style {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

还需要设置准确的宽度,溢出效果才能生效,右边可以设置固定宽度100rpx,左边宽度如何计算呢,左边文字区域宽度可以用总宽度100%- 奖牌宽度 - 头像宽度,结果就是文字宽度

由于左边部分存在层级嵌套关系,我们可以逐层计算得出文字宽度

.left计算出左边宽度width: calc(100% - 110rpx),多 10rpx 为了在左右区域之间加个间隔

.info计算出除奖牌宽度,剩余宽度width: calc(100% - 60rpx)

.name计算出除头像宽度,剩余文字宽度width: calc(100% - 70rpx)

示例部分省略代码如下

...
  <view v-for="(item, index) in otherRankList" class="item">
    <view class="left">
      <template v-if="index < 3">
        <image :src="getMedal(index)" mode="aspectFit" class="medal" />
      </template>
      <template v-else>
        <view class="rank">{{ index + 1 }}</view>
      </template>
      <view class="info">
        <image class="avatar" :src="item.avatar" mode="scaleToFill" />
        <view class="name">{{ item.name }}</view>
      </view>
    </view>
    <view class="right">{{ item.value }}</view>
  </view>
...
      .other-rank {
        border-radius: 40rpx 40rpx 0 0;
        width: 100%;
        height: 100%;
        padding: 10rpx 54rpx;
        box-sizing: border-box;
        position: relative;
        z-index: 2;

        .item {
          @include flex-center;
          position: relative;
          justify-content: space-between !important;
          gap: 10rpx;
          height: 110rpx;
          width: 100%;

          .left {
            @include flex-center;
            justify-content: flex-start !important;
            gap: 20rpx;
            width: calc(100% - 110rpx);

            .medal {
              width: 40rpx;
              height: 50rpx;
            }

            .rank {
              width: 40rpx;
              text-align: center;
              font-size: 32rpx;
              font-weight: 700;
              color: #B4B6C0;
            }

            .info {
              @include flex-center;
              justify-content: flex-start !important;
              flex: 1;
              gap: 20rpx;
              width: calc(100% - 40rpx);

              .avatar {
                width: 50rpx;
                height: 50rpx;
                border-radius: 50%;
              }

              .name {
                width: calc(100% - 50rpx);
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
              }
            }
          }

          .right {
            width: 100rpx;
            text-align: right;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
          }

        }
      }

注意

计算宽度也要把gap考虑在内,如果忽略了gap,在文字溢出时候,图片宽度会被压缩,这时候有两种方法解决

  1. 正确计算对应区域宽度,如果有gap,减去gap宽度
  2. 在被压缩的容器上添加样式flex-shrink: 0,在宽度不足时,禁止压缩

小程序动态样式

页面内如果绑定动态样式,类如

<template>
  <view :style="customStyle"></view>
</template>

<script>
  export default {
    data() {
      return {
        customStyle: {
          fontSize: '16px'
        }
      }
    }
  }
</script>

这种写法编译在微信平台并不能生效,正确写法是:style="[customStyle]"

🎬《Next 全栈 CRUD 的百老汇》

作者 LeonGao
2025年8月25日 10:55

0️⃣ 开场彩蛋:一张图先声夺人

没图?那就画一张 ASCII 海报!

┌─────────────────────────────┐
│      Next.js Full Stack     │
│         CRUD Musical        │
│                             │
│   🎤 Next   💃 Prisma   🥁 Pg │
└─────────────────────────────┘

1️⃣ 舞台搭建:项目初始化

角色 作用 安装命令
Next.js 13 App Router 前端 + API npx create-next-app@latest next-prisma-crud --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
Prisma ORM 数据舞者 npm i prisma @prisma/client
PostgreSQL 幕后仓库 Docker 一行起: docker run --name pg -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres:15

小贴士:
postgres 密码设成 postgres,就像把家门钥匙放在门垫下——方便但别上生产 🏠


2️⃣ 数据库建模:把剧本写成 schema

文件 prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String
  published Boolean  @default(false)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

小图标:📜 “Prisma Schema:把 DDL 写成十四行诗”


3️⃣ 环境变量:别让密码裸奔

.env

DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres?schema=public"

.env 加入 .gitignore,否则 GitHub 机器人会私信你:
“兄弟,密码走光了。”


4️⃣ 一键迁移:让数据库学会新舞步

npx prisma migrate dev --name init
npx prisma generate

这两行命令做了什么?

  1. migrate:把 schema 翻译成 SQL,像同声传译。
  2. generate:给 Prisma Client 生成 TypeScript 类型,像给演员发剧本。

5️⃣ 演员上场:Prisma Client 的单例模式

src/lib/prisma.ts

import { PrismaClient } from '@prisma/client';

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const prisma = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

小图标:🎭 “单例模式:确保全场只有一位女主角”


6️⃣ 全栈 CRUD:一场四幕歌剧

🎼 第 1 幕:CREATE — 新生

src/app/api/posts/route.ts

import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';

export async function POST(req: NextRequest) {
  const { title, content } = await req.json();
  const post = await prisma.post.create({ data: { title, content } });
  return NextResponse.json(post, { status: 201 });
}

前端 src/app/new/page.tsx

'use client';
import { useRouter } from 'next/navigation';

export default function NewPost() {
  const router = useRouter();
  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const form = new FormData(e.currentTarget);
    await fetch('/api/posts', {
      method: 'POST',
      body: JSON.stringify({
        title: form.get('title'),
        content: form.get('content'),
      }),
    });
    router.push('/');
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4 p-8">
      <input name="title" placeholder="标题" className="border rounded w-full" />
      <textarea name="content" placeholder="内容" className="border rounded w-full h-32" />
      <button className="bg-blue-600 text-white px-4 py-2 rounded">发布</button>
    </form>
  );
}

观众反馈:🎉 “创建成功,数据库鼓掌 1 次”


🎼 第 2 幕:READ — 回眸

src/app/api/posts/route.ts(GET)

export async function GET() {
  const posts = await prisma.post.findMany({ orderBy: { createdAt: 'desc' } });
  return NextResponse.json(posts);
}

src/app/page.tsx

export default async function Home() {
  const posts: Post[] = await (await fetch('http://localhost:3000/api/posts', { cache: 'no-store' })).json();
  return (
    <ul>
      {posts.map(p => (
        <li key={p.id} className="border-b p-4">
          <h2 className="text-xl font-bold">{p.title}</h2>
          <p>{p.content}</p>
        </li>
      ))}
    </ul>
  );
}

小图标:👀 “服务端组件:SSR 免费,不用 Vercel 也请客”


🎼 第 3 幕:UPDATE — 易容

src/app/api/posts/[id]/route.ts

export async function PUT(req: NextRequest, { params }: { params: { id: string } }) {
  const data = await req.json();
  const post = await prisma.post.update({
    where: { id: Number(params.id) },
    data,
  });
  return NextResponse.json(post);
}

前端加一个编辑按钮,路由 src/app/edit/[id]/page.tsx,复用 NewPost 组件并预填表单即可。

观众反馈:✍️ “改完标题,数据库说:我年轻了 5 岁”


🎼 第 4 幕:DELETE — 谢幕

src/app/api/posts/[id]/route.ts

export async function DELETE(_: NextRequest, { params }: { params: { id: string } }) {
  await prisma.post.delete({ where: { id: Number(params.id) } });
  return NextResponse.json({ ok: true });
}

前端点击“删除”按钮触发:

await fetch(`/api/posts/${id}`, { method: 'DELETE' });

小图标:🗑️ “数据谢幕,观众请保持安静 404”


7️⃣ 彩蛋:实时订阅,让数据库开麦

Prisma 的实时功能(预览):

const stream = prisma.post.subscribe(); // 伪代码

下一季音乐剧:WebSocket + Server Sent Events + Prisma Pulse,敬请期待 🍿


8️⃣ 谢幕清单:部署走你

  1. 推代码到 GitHub
  2. 一键 Vercel:选 Postgres 模板,自动注入 DATABASE_URL
  3. 看日志:
    [POST] /api/posts 201 23ms
    [GET]  /api/posts 200 15ms
    

观众鼓掌:👏 “从本地 3000 到线上 HTTPS,只花了 30 秒”


9️⃣ 彩蛋台词墙

角色 经典台词
Prisma Client “我不是 ORM,我是类型安全的舞伴。”
Next.js “App Router:服务端组件,免费 SSR,不加价。”
PostgreSQL “索引就像高跟鞋,漂亮但别乱穿。”

“愿你每次 npx prisma migrate 都能顺利通过,
愿每个 404 都变成 200 OK。”
—— 一名把 SQL 跳成芭蕾舞的全栈工程师 🩰

🎭 一场浏览器里的文艺复兴

作者 LeonGao
2025年8月25日 10:47

1. 开场:把 AI 请进网址栏

公元前 2025 年,一位前端工程师在键盘上敲下 npm create aigc-app@latest
浏览器瞬间变成了一座 24h 不打烊的剧院:

  • 观众是用户
  • 演员是 AI
  • 舞台是 Web

下面这份节目单,记录了最值得一聊的 Web 技术——它们就像后台道具师,默默把 AI 的魔法变成可点击的像素。


2. 道具一:WebGPU —— 把显卡借给 AI

关键台词 “亲爱的显卡,别只打游戏了,来跑个 7B 大模型吧!”
底层原理 浏览器→WebGPU→驱动→显卡→矩阵运算→AI 推理
生活比喻 把家里的台式机 GPU 像共享单车一样扫二维码骑走
// 1行代码,显卡到手
const adapter = await navigator.gpu.requestAdapter();

小贴士:
如果看到 undefined,说明浏览器还在劝你“显卡太老,先去喝杯咖啡”。


3. 道具二:WebAssembly + SIMD —— CPU 也想加班

关键台词 “CPU 大喊:我还能再战十年!”
底层原理 C/C++ 编译成 .wasm,SIMD 一次算 128 bit,像 4×4 拼乐高
// 加载 wasm,给 CPU 打鸡血
const wasmModule = await WebAssembly.instantiateStreaming(
  fetch('model_simd.wasm')
);

小图标:⚡️ “单核变八核,电费不变”


4. 道具三:Web Workers + SharedArrayBuffer —— 多线程不堵车

关键台词 “主线程:你慢慢算,我去刷 UI。”
底层原理 主线程 ↔ SharedArrayBuffer ↔ Worker ↔ AI 模型
生活比喻 老板(主线程)把报表丢给实习生(Worker),自己去开会
// 主线程
const worker = new Worker('aigc.worker.js');
worker.postMessage({ cmd: 'dream', prompt: '赛博猫咪' });

// worker.js
self.onmessage = ({ data }) => {
  const pixels = bigModel.inference(data.prompt);
  self.postMessage(pixels, [pixels.buffer]); // 零拷贝传输
};

5. 道具四:WebRTC + WebTransport —— 把服务器搬进浏览器

关键台词 “别再问我服务器在哪,它就在隔壁标签页!”
底层原理 P2P 数据通道 → UDP/QUIC → 低延迟流式推理
生活比喻 像打电话,但双方都是浏览器
// 建立 P2P 推理通道
const pc = new RTCPeerConnection();
const dataChannel = pc.createDataChannel('ai-stream');
dataChannel.onmessage = ({ data }) => drawImage(data);

6. 道具五:Web Codecs —— 让像素学会压缩瑜伽

关键台词 “一张 4K 图,秒变表情包。”
底层原理 浏览器原生硬件编解码 → H.264/AV1/VP9
生活比喻 把大象塞进冰箱,但冰箱自带黑洞压缩器
const decoder = new VideoDecoder({
  output: frame => canvas.drawImage(frame),
  error: e => console.error('解码器罢工', e)
});
decoder.configure({ codec: 'avc1.42E01E' });

7. 道具六:IndexedDB + OPFS —— 浏览器里藏个硬盘

关键台词 “模型 2 GB?本地缓存,下次秒开。”
底层原理 浏览器沙盒文件系统 → 异步 IO → 零拷贝映射
生活比喻 把模型当离线地图,没网也能导航
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('llama2_7b.bin', { create: true });
const writable = await fileHandle.createWritable();
await writable.write(modelBlob);
await writable.close();

8. 彩蛋:一行代码的 AI 魔法秀

把上文所有技术串成一串糖葫芦,只需:

// index.html
<canvas id="stage"></canvas>
<script type="module">
  import { AIGen } from './aigc.min.js';
  await AIGen.init({ gpu: true, wasmSimd: true, cache: true });
  AIGen.paint('月光下的独角兽', document.getElementById('stage'));
</script>

浏览器:“灯光、显卡、Action!”


9. 谢幕 & 加餐

下一站 学习资料
WebGPU 入门 gpuweb.github.io
wasm SIMD github.com/WebAssembly…
WebRTC datachannel webrtc.github.io/samples

小彩蛋:把本文打印出来折成纸飞机,飞得越远,模型跑得越快 🛩️


“愿你写的每一个 await,都能等到未来的惊喜。”
—— 一个把 AI 塞进地址栏的工程师

组件基础-List&Tabs

作者 缘澄
2025年8月25日 10:38

一、List

列表组件

结构:

@Entry
@Component
struct ListPage {

  build() {
    List() {
      ListItem() {
        Text("子组件")
      }
      ListItem()
      ListItem()
      ListItem()
    }
    .height('100%')
    .width('100%')
  }
}
  • 列表中的内容一般是相似的,因此我们可以利用ForEach来进行渲染,减少代码量
  • 当数据量过大时,我们就需要需要使用LazyForEach来提升效率,增加用户体验

ForEach(数据源, 组件生成函数, 键值生成函数) 键值生成函数是一个回调函数,用于生成唯一的key;若不写,系统会帮我们生成独一无二的key,这个参数,宁可不给也不要随意添加,不恰当会影响运行效率

image-20250825093718731.png

interface testListData {
  name: string
  age: number
}


@Entry
@Component
struct ListPage {
  @State data: testListData[] = [
    { name: "a", age: 12 },
    { name: "b", age: 13 },
    { name: "c", age: 14 },
    { name: "d", age: 15 },
    { name: "e", age: 16 },
  ]

  build() {
    List({ space: 5 }) {
      ForEach(this.data, (item: testListData, idx: number) => {
        ListItem() {
          Column() {
            Row() {
              Text(item.name).fontSize(30)
              Blank()
              Text(item.age + "").fontSize(30)
            }
            .width('100%')

            Divider().strokeWidth(2)
          }
          .width('100%')
        }
      }, (item: testListData, idx) => idx + "")
    }
    .height('100%')
    .width('100%')
  }
}

二、Tabs

类似于微信底部的切换栏

image-20250825094614285.png

切换栏默认是在顶部的,可以通过Tabs({barPosition: BarPosition.End})设置栏的位置为底部

image-20250825095012444.png

通过设置controller: this.barController给tabs设置控制器,方便后续的手动设置操作

.barMode(BarMode.Scrollable)// 滚动

@Entry
@Component
struct TabsPage {
  build() {
    Column() {
      TabsComponents()
    }
    .height('100%')
    .width('100%')
  }
}

@Component
struct TabsComponents {
  @State currentIdx: number = 0
  barController: TabsController = new TabsController()

  @Builder
  Bar(tabBarName: string, idx: number) {
    Text(tabBarName).fontSize(20)
      .fontColor(this.currentIdx === idx ? Color.Red : Color.Black)
      .onClick(() => {
        this.currentIdx = idx
        this.barController.changeIndex(this.currentIdx)
      })
  }

  build() {
    Column() {
      Tabs({ barPosition: BarPosition.End, controller: this.barController }) {
        TabContent() {
          Text("界面1").fontSize(60)
        }
        .tabBar(this.Bar("界面1", 0))
        TabContent() {
          Text("界面2").fontSize(60)
        }
        .tabBar(this.Bar("界面2", 1))
        TabContent() {
          Text("界面3").fontSize(60)
        }
        .tabBar(this.Bar("界面3", 2))
      }
    }
  }
}
  • 绑定的目标页数一定要绑定@State装饰器,否则只切换无效果@State currentIdx: number = 0

image-20250825102759325.png

  • 缺失@State

image-20250825103330813.png

揭秘DOM键盘事件:从基础到高级技巧全解析!

作者 coding随想
2025年8月25日 10:35

在前端开发中,键盘事件是用户交互的重要组成部分。无论是实现快捷键、实时搜索,还是复杂的表单验证,键盘事件都扮演着核心角色。今天,我们将深入探讨DOM事件中的键盘与输入事件,揭开它们的神秘面纱,掌握其背后的原理与实战技巧。


一、键盘事件的定义与分类

1. 什么是键盘事件?

键盘事件是用户在使用键盘与页面交互时触发的事件。常见的键盘事件包括:

  • keydown:按键被按下时触发(按住不放会重复触发)。
  • keypress:字符键被按下时触发(功能键如方向键不会触发)。
  • keyup:按键被松开时触发。

2. 事件触发顺序

当用户按下字符键(如字母、数字)时,事件触发顺序为:

keydown → keypress → keyup

而对于功能键(如方向键、Ctrl、Shift),仅触发 keydownkeyup


二、键盘事件的核心属性

1. keyCodewhich

  • keyCode:返回按键的ASCII码值(兼容性较差,现代浏览器已逐渐弃用)。
  • which:与 keyCode 类似,但兼容性更好(Firefox 2.0+ 支持)。
  • 兼容写法
    const keyCode = e.keyCode || e.which;
    

2. keycode

  • key:返回实际按下的字符(如 "a"、"Enter"),区分大小写。
  • code:返回物理按键的代码(如 "KeyA"、"ArrowUp"),不区分大小写。
    console.log(e.key);   // 输出 "a"
    console.log(e.code);  // 输出 "KeyA"
    

3. 组合键判断

通过以下属性判断是否按下了组合键:

  • altKey:Alt 键是否按下。
  • shiftKey:Shift 键是否按下。
  • ctrlKey:Ctrl 键是否按下。
  • metaKey:Mac 上的 Command 键是否按下。
    if (e.ctrlKey && e.key === "s") {
      console.log("你按下了 Ctrl + S");
    }
    

三、常用方法与实战技巧

1. 绑定事件

  • 传统方式
    document.onkeydown = function(e) {
      console.log("按键按下");
    };
    
  • 现代方式
    document.addEventListener("keydown", function(e) {
      console.log("按键按下");
    });
    

2. 实时搜索功能

通过 keyup 事件实现实时搜索:

document.getElementById("search").addEventListener("keyup", function(e) {
  const query = this.value;
  console.log("搜索内容:", query);
});

3. 快捷键设计

为页面添加快捷键,例如按 Ctrl + S 保存:

document.addEventListener("keydown", function(e) {
  if (e.ctrlKey && e.key === "s") {
    e.preventDefault(); // 阻止默认行为(如浏览器保存)
    console.log("保存操作");
  }
});

4. 组合键判断

判断用户是否按下了组合键:

document.addEventListener("keydown", function(e) {
  if (e.altKey && e.key === "a") {
    console.log("你按下了 Alt + A");
  }
});

四、应用场景

1. 游戏开发

  • 使用方向键或 WASD 控制角色移动。
  • 按空格键跳跃或射击。

2. 表单验证

  • 实时检测用户输入是否符合格式(如邮箱、手机号)。
  • 按回车键提交表单。

3. 快捷键优化

  • 提升用户操作效率(如 Markdown 编辑器中的快捷键)。
  • 自定义快捷键(如 IDE 中的 Ctrl + C/V/X)。

4. 无障碍设计

  • 为残障用户提供键盘导航支持(如通过 Tab 键切换焦点)。

五、注意事项与性能优化

1. 元素的可聚焦性

  • 非表单元素(如 div:默认无法触发键盘事件,需设置 tabindex 属性:
    <div tabindex="0" id="myDiv"></div>
    
  • 动态聚焦:通过 element.focus() 方法主动获取焦点。

2. 性能问题

  • 节流(Throttle):对于高频触发的 keydown/keypress 事件,使用节流函数避免页面卡顿。
    function throttle(fn, delay) {
      let last = 0;
      return function(...args) {
        const now = Date.now();
        if (now - last > delay) {
          last = now;
          fn.apply(this, args);
        }
      };
    }
    
    document.addEventListener("keydown", throttle(function(e) {
      console.log("节流后的按键事件");
    }, 300));
    

3. 兼容性处理

  • 移动端适配:部分移动设备对 keypress 事件支持较差,建议优先使用 keydownkeyup
  • 阻止默认行为:某些按键(如回车键)可能触发默认操作,需通过 e.preventDefault() 阻止。

六、经典案例解析

1. 模拟京东搜索框快捷键

当用户按下 S 键时,自动聚焦搜索框:

document.addEventListener("keydown", function(e) {
  if (e.key === "s" && !e.ctrlKey && !e.metaKey) {
    document.getElementById("search").focus();
  }
});

2. 组合键实现保存功能

按下 Ctrl + S 时保存数据:

document.addEventListener("keydown", function(e) {
  if (e.ctrlKey && e.key === "s") {
    e.preventDefault();
    console.log("数据已保存");
  }
});

七、总结

键盘事件是前端交互中不可或缺的一部分,掌握其原理与技巧能显著提升用户体验。从基础的按键判断到复杂的组合键设计,再到性能优化与无障碍适配,每一个细节都值得深入探索。通过本文的讲解,相信你已经对键盘事件有了全面的理解。接下来,不妨动手实践,将这些技巧应用到你的项目中,创造更高效的用户交互体验!

❌
❌