普通视图

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

如何用一份 JSON 配置搞定“法律计算器”的动态表单

2026年3月3日 15:18

引言:小明的工伤赔偿奇遇记

想象一下,当事人小明打开我们的“法律计算器”小程序,想算算工伤赔偿。

  • 场景 A:他手抖选了“劳动关系”,页面立刻弹出“月工资是多少?”;
  • 场景 B:他改主意选了“交通事故”,页面瞬间变身,开始问“伤残等级”;
  • 场景 C:他填了个“30000”的月薪,系统立马提示:“哥们,这超过社平工资 3 倍了,你确定没填错?”(后端异步校验)。

作为开发者,你是不是已经开始头疼了?如果针对劳动争议、交通事故、借贷纠纷等等法律业务都分别写一个 .vue 页面,光是维护 v-if/v-else 就得掉一半头发。万一明天产品经理说:“在这个表单中间加个‘案发地点’字段”,你是不是得发版重新提审?

拒绝写死代码! 今天我们来聊聊如何用 数据驱动UI 架构,打造一个“千人千面”的法律计算器。


一、 核心原理:把前端做成“乐高底板”

在传统的开发模式中,前端是“建筑师”,负责设计页面结构(Template);后端只是“搬砖工”,负责提供数据(Data)。

但在 数据驱动UI 架构下,角色反转了。前端退化成了一块纯粹的 “乐高底板”,而后端发来的 JSON 配置,就是那张 “搭建图纸”。前端不关心业务逻辑,只负责一件事:给什么积木,就搭什么房子。

1. 渲染引擎:v-for 的魔法

让我们看看核心渲染引擎 customForm/index.vue 是如何工作的。它的核心逻辑极其简单,就像是在遍历一份清单:

<!-- components/customForm/index.vue (精简版) -->
<template>
  <view class="custom-form">
    <!-- 第一层循环:遍历表单分组 (Section) -->
    <view v-for="(section, sIndex) in formConfig" :key="sIndex">
      <view class="section-title">{{ section.title }}</view>
      
      <!-- 第二层循环:遍历具体的题目 (Items) -->
      <view v-for="(item, iIndex) in section.items" :key="iIndex">
        
        <!-- 积木 A:普通输入框 (component === 3) -->
        <u-form-item v-if="item.component === 3" :label="item.title">
          <u-input v-model="formData[item.qId]" />
        </u-form-item>

        <!-- 积木 B:选择器 (component === 4) -->
        <u-form-item v-if="item.component === 4" :label="item.title">
          <view @click="openPicker(item)">{{ getLabel(item) }}</view>
        </u-form-item>

        <!-- 积木 C:复杂的利率选择器 (component === 9) -->
        <rate-selector 
          v-if="item.component === 9" 
          :init-value="formData[item.qId]"
        />
        
      </view>
    </view>
  </view>
</template>

前端不再写死 <input><select>,而是根据 JSON 中的 component 字段(3 代表输入框,4 代表选择器,9 代表复杂组件)动态决定渲染什么。

2. 每次修改表单都动态获取JSON数据

最精彩的部分来了。既然前端不写 v-if="salary > 30000",那条件分支怎么实现?

答案是:不要在前端做逻辑判断,把用户的每一次交互都告诉后端。

这是一个 (问后端) 的过程。在具体业务代码中,我们监听了表单的每一次变更:

// pages/enterpriseLaw/legalCalculator/form.vue

// 1. 用户修改了答案
handleFormChange(newAnswer) {
  // 更新本地答案池
  this.updateAnswers(newAnswer);
  
  // 2. 核心:带着当前的答案,去问后端“下一步该展示什么?”
  this.getDynamic(); 
},

async getDynamic() {
  // 3. 调用接口,把所有已填答案扔给后端
  const payload = { 
    appId: this.appId, 
    answers: this.answers 
  };
  
  // 4. 后端的大脑开始飞速运转,计算出新的题目列表
  const res = await this.$api.getDynamic(payload);
  
  // 5. 前端拿到新的 JSON,Vue 自动 diff 更新视图
  this.questions = res.data.questions;
}

点睛之笔:这就是“一份 JSON 配置”的真相。逻辑在后端,前端只是负责画图的“画笔”。 这样一来,无论是增加题目、修改逻辑分支,还是调整校验规则,都只需要后端改配置,前端代码一行都不用动!


二、 难点攻克:细节决定成败

痛点一:嵌套条件分支的“配置化”

如果题目之间有复杂的嵌套关系(例如 是否有借款 -> 有几笔 -> 第一笔利息 -> 怎么算的),JSON 结构该怎么设计?

我们采用 Section (分组) -> Group (实例) -> Items (题目) 的三层结构。Vue 的响应式系统在这里帮了大忙。当后端返回的 questions 数组发生变化(比如因为你选了“有借款”,数组里多了一个“借款详情”的 Section),Vue 会自动检测到数据的变化,并高效地修补 DOM。

// 后端返回的 JSON 结构示意
[
  {
    "groupTitle": "基本信息",
    "items": [ ... ]
  },
  {
    "groupTitle": "借款详情", // 只有当用户选了“有借款”才会返回这个 Section
    "isGroup": true,        // 标记为可重复的分组(如多笔借款)
    "items": [ ... ]
  }
]

痛点二:无缝嵌入异步校验

有些校验前端做不了,比如“赔偿金是否符合当地最新的法律标准”。这时候,我们需要把校验权也交给后端。

在代码中,我们设计了一个巧妙的 backendErrors 机制:

  1. 用户填完:触发 validateByBackend
  2. 后端校验:发现 Q101 题目的金额填错了,返回错误 Map:{ "q_101": "金额过大,请确认" }
  3. 前端标红
// customForm/index.vue

// 监听后端传来的错误对象
props: ['backendErrors'],

// 在模板中精准展示错误
<view v-if="backendErrors[item.qId]" class="backend-error">
  {{ backendErrors[item.qId] }}
</view>

这样,异步的业务校验就像本地校验一样自然流畅,用户根本感觉不到请求的延迟。

痛点三:原子组件的扩展(RateSelector)

这时候有人会问:“如果我要一个超级复杂的组件,比如‘LPR 利率计算器’,JSON 配置能描述清楚吗?”

当然可以!这就是 数据驱动UI 的灵活性。我们不需要用 JSON 描述组件内部的每一个 div,而是把这个复杂组件封装成一个原子积木

看看 components/customForm/RateSelector.vue,它内部包含了:

  • 日/月/年利率的切换
  • 百分比/千分比的换算
  • LPR 动态查询

但在表单引擎眼里,它只是一个普通的积木:

// 如果 component 代码是 9,我就渲染 RateSelector
<rate-selector v-if="rawItem.component === 9" ... />

这样,我们既保持了引擎的通用性(处理普通输入框),又保留了处理复杂业务的能力(通过自定义组件扩展)。


三、 总结:从“搬砖”到“搭积木”

数据流转图

最后,让我们用一张图来总结整个流程:

graph TD
    User[用户输入] -->|触发| Event[handleFormChange]
    Event -->|携带 Current Answers| API[调用 getDynamic 接口]
    API -->|发送至| Server[后端逻辑大脑]
    Server -->|计算条件分支/校验| Config[生成新的 JSON 配置]
    Config -->|返回| Frontend[前端 Vue 引擎]
    Frontend -->|Vue Reactivity| DOM[界面无感刷新]
    DOM -->|展示| User

架构优势

  1. 配置热更新: 运营人员想在表单里加一个“备注”字段?改一下数据库里的 JSON 配置就行了。用户端不需要发版,不需要更新,打开小程序就能看到新字段。这在法律法规频繁变动的行业简直是救命稻草。

  2. 逻辑复用: 我们只写了一套 customForm 引擎,却同时支持了“劳动争议”、“交通事故”、“民间借贷”等等法律业务的计算器。每个计算器只是后端数据库里的一条配置记录而已。

“偷懒”是程序员的第一生产力。 把复杂的逻辑甩给后端,把繁琐的渲染交给引擎,我们前端开发者,终于可以安心地喝一杯咖啡了。☕️


如果你对这套代码感兴趣,欢迎在评论区留言

defineModel 是进步还是边界陷阱?双数据源组件的选择逻辑

2026年3月3日 15:15

defineModel 是 Vue 3.4 引入的语法糖。

它看起来只是让 v-model 更优雅:

const visible = defineModel<boolean>('visible')

但它背后做的事情,远不止简单的语法糖,甚至改变了组件的状态哲学

传统 v-model 的“单一数据源”假设

在大部分 v-model 语义里,存在一个隐含规则:

只传 prop,不监听 update 事件 = 组件不可更新。

比如对弹窗组件,如果父组件只传递了 visibleprop

<MyDialog :visible="visible" />

我们会认为 MyDialog 的显示和隐藏完全由父组件控制

父组件的 visible 变量是控制 MyDialog 显示/隐藏的唯一数据源

这是一种非常清晰的“受控组件”边界

defineModel 在子组件中引入的本地数据源

但是如果 MyDialog.vuevisibledefineModel 实现时,情况会有些不一样:

<script setup>
  const visible = defineModel('visible')
</script>

<template>
  <div>
    <div>MyDialog 内的 visible:{{ visible }}</div>
    <button @click="visible = !visible">MyDialog 内切换 visible</button>
  </div>
</template>

如果父组件中还是

<MyDialog :visible="visible" />

请问子组件按钮点击时,visible 会变化吗?

答案是:会变化

switch.gif

代码链接

defineModel 做了什么?

直接看 playground 生成的代码:

image.png

defineModel 除了生成对应的 propsemits,还通过 useModel 产生了 MyDialog 内的 visible 变量。

而在 useModel 里,会使用 customRef 创建一个本地变量。

在这个本地变量的设值逻辑里,是这样的(简化):

if (
  !(
    rawProps &&
    // check if parent has passed v-model
    (name in rawProps ||
      camelizedName in rawProps ||
      hyphenatedName in rawProps) &&
    (`onUpdate:${name}` in rawProps ||
      `onUpdate:${camelizedName}` in rawProps ||
      `onUpdate:${hyphenatedName}` in rawProps)
  )
) {
  // no v-model, local update
  localValue = value
  trigger()
}

:visible="visible"@update:visible="..." 任意一个不存在,就会更新本地数据。

翻译一下:

只有父组件同时提供 “prop + @update”,子组件才会始终使用父组件的值

否则 —— 子组件会使用本地变量的数据

useModel 的动态数据源

这意味着:

父组件传入的数据,并不一定是最终数据源。

真正的数据源变成:

  • 有监听 → 父组件
  • 无监听 → 子组件本地

这是一种动态切换的数据源模型。

这是不是问题?

从功能角度看,它很强大。

优点

  • 支持“受控 / 非受控”自动切换
  • 多 model 场景写法更优雅

对于“有内部状态”的组件,非常舒服。比如手风琴组件,使用方不需要提供变量保存手风琴的开关状态。

但对于大部分输入组件,它带来了新的权衡。

模糊了边界

传统设计下:

只传 prop = 组件不可修改

现在:

只传 prop ≠ 不可修改

如果你想让组件真正受控,你必须写:

<MyDialog
  :visible="visible"
  @update:visible="() => {}"
/>

用一个空监听器,强制关闭本地数据源。

这就产生了一个认知断层:

  • 使用者需要知道组件内部是否用了 defineModel
  • 否则无法判断它是否会维护本地状态

组件的状态模型,不再从接口上显式表达。

语义变化

<MyDialog :visible="visible" />

由原本的只读受控语义,隐式拓展出了类似 init-visible 的初始值赋值语义。

而是受控,还是初始值赋值,取决于内部是否使用了 defineModel

总结与想法

defineModel 带来的不只是语法糖。还把

数据源从“静态归属”变成了“动态判断”。

它让 Vue 组件具备了“双数据源能力”。需要清醒认知它的能力边界。

defineModel 并没有让 v-model 更简单,反而让组件的状态模型更复杂。

两个想法:

  • 对于普通的输入组件,尽量避免使用 defineModel,保持单一数据源
  • 在状态不一致的问题排查上,需要考虑缺少监听器引发的问题

要闹哪样?又出现了一款新的格式化插件,尤雨溪力荐,速度提升了惊人的45倍!

作者 李剑一
2026年3月3日 15:05

前两天刚刚讨论完Vize(参考这篇文章: # 前端圈子又出新东西了,大幅提升解析速度。尤雨溪推荐,但我不太推荐),这两天发现前端又出现新工具了,而且是尤大力荐的,我得到这个消息还算是比较晚的了。

其实这款插件早已官宣,最最关键的一点是,它的速度比咱们常用的Prettier快了整整45倍。

今天咱们简单看一下这款插件 —— oxfmt

image.png

背景

其实前端最近几年一直在致力于底层的革新,原因也非常简单,Js在系统中的运行效率和编解码速度远逊于Rust这样的语言。

所以Vite中的 Rollup 变成了 Rolldownesbuild 变成了 Oxc

大家可能不太清楚 Oxc 是啥,咱们简单过一下。

image.png

OxcVoidZero 团队(Vite 核心团队,尤雨溪的公司)用 Rust 开发的 JS/TS 全链路工具链。

简单说就是以后前端的底层部分全都用 Rust 写了,补齐了 Oxc 以后,Rust在前端领域实现了全替换。

带来的好处不言而喻,首先是速度。

作为编译型语言,Rust 的执行效率接近 C/C++,相比传统前端工具的 JS/Go 实现构建 / 转译速度提升 数倍到数十倍。

并且内存占用降低 50%+,大型项目不会出现 JS 工具的内存溢出 / 卡顿问题,真正意义上实现了闪电般的加载速度

其次做为底层语言,Rust 的所有权、借用检查机制从语法层面杜绝空指针、内存泄漏等常见问题,前端工具的崩溃率、异常率大幅降低,尤其适合大型工程化场景。

最关键的一点,Rust 编译出的二进制文件无需依赖 Node.js 运行时,在 Windows/macOS/Linux 上的执行逻辑、性能表现高度一致。

解决了 JS 工具在不同系统下的兼容性问题,完美解决了跨平台一致性的问题。

Oxfmt

理解了Oxc就能简单说说 Oxfmt 了,Oxfmt 是 Oxc 生态中的代码格式化工具,也是目前已经基本上完成的 Rust 替换 Prettier 的例子。

image.png

Oxfmt Beta 几个关键词过一下:

  • 100% Prettier 兼容,无缝迁移
  • 支持 --migrate-prettier
  • 支持更丰富的文件格式
  • Import 自动排序
  • package.json 自动排序
  • Node.js API
  • IDE 完美支持

因为本身 Oxfmt 就可以看作是 Prettier 的Rust版本,所以团队在开发的时候也选择了对 Prettier API的完全兼容,所以开发者一般是没啥感知的。

你能够感受到的也就是快!

尝鲜

安装 Oxfmt

pnpm add -D oxfmt

这里需要给 oxfmt 配置一下脚本,找到 package.json

{
  "scripts": {
    "fmt": "oxfmt",
    "fmt:check": "oxfmt --check"
  }
}

现在已经可以用了。

# 格式化文件
pnpm run fmt

# 检查格式,但不修改文件
pnpm run fmt:check

以上是比较粗浅的应用,真正想要实现项目内详细可用还需要创建一下配置文件,oxfmt 默认使用 .oxfmtrc.json 作为配置文件。

# 初始化配置文件
oxfmt --init

# 从Prettier迁移
oxfmt --migrate prettier

# 全量格式化
npx oxfmt . --write

工程化应用

日常项目开发过程中主要是保存、提交的时候自动格式化,这个场景应用的比较多。

oxfmt 在 vscode 中可以通过 Oxfmt 官方扩展实现保存格式化。

首先安装 Oxfmt 官方插件,搜索 Oxc 即可。

image.png

.vscode/settings.json 中添加以下配置:

{
    "editor.formatOnSave": true,
    "[vue]": {
        "editor.defaultFormatter": "oxc.oxfmt"
    },
    "[javascript]": {
        "editor.defaultFormatter": "oxc.oxfmt"
    },
    "[typescript]": {
        "editor.defaultFormatter": "oxc.oxfmt"
    }
}

之前用 Prettier 的同学记得关掉,避免冲突。

提交格式化可以通过 pre-commit 钩子实现:

pnpm install -D husky lint-staged
npx husky install
# 添加 pre-commit 钩子
npx husky add .husky/pre-commit "npx lint-staged"

同时在 package.json 中增加相应配置:

{
    "lint-staged": {
        "*.{js,ts,vue,json,css,scss,md}": [
        "oxfmt --write"
        ]
    }
}

总结

我个人比较建议大家从现在开始就把 Prettier 替换为 Oxfmt,原因主要有三:

  • Rust实现前端底层已成为大趋势,未来一定是这套工程大一统。
  • 速度更快,内存用的更少,Vite团队开发。
  • 确实好用,接近无感的存在。

借助VTable Skill实现10W+数据渲染

作者 核以解忧
2026年3月3日 14:22

前言

借助VTable Skill实现vtable的基础功能

SKILL作用

输入关键词后结合skill快速的生成我们需要的内容,把“经验手册”变成 AI 可以读取和执行的能力结构。通过Skill,开发者无需记忆繁琐的API文档,只需用自然语言描述需求,AI就能基于VTable的最佳实践生成高质量代码。

安装vtable skill

npx skills add VisActor/VChart 或者

npx skills add [GitHub - VisActor/VChart: VChart, more than just a cross-platform charting library](https://github.com/visactor/vchart) --skill vchart-development-assistant

进行安装在 Cursor、Trae等支持 skills 的 AI 编辑器中使用。

image.png

将技能安装到项目的 .``XXX``/skills 目录下,如下图

image.png

快速上手vtable

安装vtable包和准备项目结构

使用 npm 安装

npm install @visactor/vtable 

使用 yarn 安装

yarn add @visactor/vtable

生成基础表格

开始之前,我们先来看一下SKILL里面的文档

image.png

我用的是Trae,我们提前配置好智能体和模式

image.png

根据skill的用户意图关键词和查询规则,我们输入以下内容:

结合skill技能,创建一个基本表格

可以看到,AI会根据技能,查找对应的md文件,生成如下内容:

image.png

image.png

数据、列、主题处理

接下来,我们通过更复杂的指令来完善表格功能: 结合skill,我的数据有10万条,列有10列,姓名列固定,主题使用默认主题 AI会根据Skill中的性能优化指南,生成适合大数据量展示的表格配置,包括虚拟滚动、列固定等特性: image.png

固定列的时候发现AI处理成了固定2列,姓名在第二列 需要固定前两列,这里要手动处理一下:

image.png

252.gif

为了满足更复杂的展示需求,我们需要对某些列进行复杂的业务处理: 薪资列需要自定义渲染,薪资超过8000的字体变红,超过1万的背景色变红 字体白色 AI会利用VTable的自定义渲染能力,生成满足条件的单元格样式配置:

image.png

image.png

到此,一个具备大10W+数据渲染的表格就完成了。

总结

AI也不是万能的,有时候生成的代码跟你想要的有一丢丢出入,比如我那个固定列的问题,稍微手动调一下就好。但总体来说,以前写个表格要半小时,现在五分钟搞定,剩下的时间摸鱼不香吗?

参考资料:

vtable官网: visactor.com/vtable/exam…

VTable Skill GitHub: github.com/VisActor/VC…

Trae参考文档: docs.trae.ai/ide/skills?…

Cursor参考文档:cursor.com/cn/docs/con…

React Context 详解:从入门到性能优化

作者 阿虎儿
2026年3月3日 14:21

React Context 详解:从入门到性能优化

本文适合熟悉 Vue 但刚开始学习 React 的开发者,通过 Vue 的 provide/inject 对比来理解 React Context。

一、什么是 Context?

在组件开发中,我们经常遇到这样的场景:某个数据需要在多层嵌套的组件间共享。如果一层层通过 props 传递,代码会变得非常冗长且难以维护,这就是所谓的 "prop drilling" 问题。

React 的 Context 和 Vue 的 provide/inject 都是为了解决这个问题而设计的 —— 它们允许数据跨层级传递,跳过中间组件。

二、React Context 基础用法

核心三步

  1. 创建 Context —— 创建一个数据共享的"通道"
  2. 提供数据 —— 父组件通过 Provider 提供数据
  3. 消费数据 —— 子组件通过 useContext 获取数据

完整示例

// ========== 1. 创建 Context ==========
// context.tsx
import { createContext, useContext } from 'react'

// 定义数据类型
type MyContextValue = {
  name: string
  age: number
}

// 创建 Context(可设置默认值)
const MyContext = createContext<MyContextValue>({ name: '', age: 0 })

// 导出一个 hook 方便使用
const useMyContext = () => useContext(MyContext)

export { MyContext, useMyContext }
// ========== 2. 父组件提供数据 ==========
// parent.tsx
import { MyContext } from './context'
import Child from './child'

const Parent = () => {
  const data = { name: '张三', age: 18 }

  return (
    <MyContext.Provider value={data}>
      <Child />
    </MyContext.Provider>
  )
}
// ========== 3. 子组件消费数据 ==========
// child.tsx
import { useMyContext } from './context'

const Child = () => {
  const { name, age } = useMyContext()
  
  return <div>{name} - {age}岁</div>
}

三、对比 Vue 的 provide/inject

如果你熟悉 Vue,这个概念其实非常相似:

步骤 React Vue
创建 createContext() 无需显式创建
提供 <Context.Provider value={}> provide(key, value)
消费 useContext(Context) inject(key)

Vue 等价写法

<!-- 父组件 -->
<script setup>
import { provide } from 'vue'
import Child from './child.vue'

const data = { name: '张三', age: 18 }
provide('myContext', data)
</script>

<template>
  <Child />
</template>
<!-- 子组件 -->
<script setup>
import { inject } from 'vue'

const { name, age } = inject('myContext')
</script>

<template>
  <div>{{ name }} - {{ age }}岁</div>
</template>

可以看到,两者的设计思想是一致的,只是语法不同:

  • React 使用 JSX 的组件包裹方式 <Context.Provider>
  • Vue 使用 Composition API 的函数调用方式

四、原生 Context 的性能问题

原生 React Context 存在一个性能陷阱:

只要 Context value 中的任何一个字段变化,所有消费这个 Context 的组件都会重新渲染,即使它们只用到了没变的字段。

// 原生 React Context 的问题
const MyContext = createContext({ name: '张三', age: 18, city: '北京' })

// 这个组件只用 name,但 age 或 city 变化时也会重新渲染!
const Child = () => {
  const { name } = useContext(MyContext)
  return <div>{name}</div>
}

当 Context 中有几十个字段时(这在大型应用中很常见),这个问题会严重影响性能。

五、use-context-selector:性能优化方案

为了解决这个问题,社区提供了 use-context-selector 库。它支持选择器模式,让组件只订阅自己关心的字段。

安装

npm install use-context-selector

使用方式

// 从 use-context-selector 导入,而不是 react
import { createContext, useContext } from 'use-context-selector'

const MyContext = createContext({ name: '张三', age: 18, city: '北京' })

// 使用选择器,只订阅 name
const Child = () => {
  const name = useContext(MyContext, v => v.name)  // age 或 city 变化不会触发重渲染
  return <div>{name}</div>
}

核心区别

特性 React 原生 use-context-selector
导入来源 'react' 'use-context-selector'
更新粒度 整个 Context 变化就重渲染 可以用选择器精确订阅某个字段
性能 大型 Context 可能性能差 优化了选择器模式,避免不必要的重渲染
使用方式 useContext(ctx) useContext(ctx, selector?)

六、实际案例分析

以 Dify 项目中的 ChatWithHistoryContext 为例:

// context.tsx
import { createContext, useContext } from 'use-context-selector'

export type ChatWithHistoryContextValue = {
  appMeta?: AppMeta | null
  appData?: AppData | null
  appParams?: ChatConfig
  currentConversationId: string
  conversationList: AppConversationData['data']
  handleNewConversation: () => void
  handleChangeConversation: (conversationId: string) => void
  // ... 还有 20+ 个字段
}

export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
  currentConversationId: '',
  // ... 默认值
})

export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)
// parent.tsx - 提供数据
const ChatWithHistoryWrap = () => {
  const contextValue = useChatWithHistory()  // 获取所有数据

  return (
    <ChatWithHistoryContext.Provider value={contextValue}>
      <ChatWithHistory />
    </ChatWithHistoryContext.Provider>
  )
}
// child.tsx - 消费数据
const ChatWithHistory = () => {
  const { 
    appData, 
    conversationList, 
    handleChangeConversation 
  } = useChatWithHistoryContext()
  
  // 使用数据...
}

这个 Context 有 30+ 个字段,如果使用原生 Context,任何一个字段变化都会导致所有子组件重渲染。使用 use-context-selector 后,框架内部做了优化,避免了不必要的渲染。

七、数据流图解

┌─────────────────────────────────────────────────┐
│  ChatWithHistoryWrap (父组件)                    │
│                                                 │
│  通过 useChatWithHistory() 获取所有数据          │
│  { appData, appParams, conversationList, ... }  │
└─────────────────────────────────────────────────┘
                        │
                        ▼
┌─────────────────────────────────────────────────┐
│  ChatWithHistoryContext.Provider                │
│  value={{ appData, appParams, ... }}            │  ← 数据注入到 Context
└─────────────────────────────────────────────────┘
                        │
                        ▼
┌─────────────────────────────────────────────────┐
│  ChatWithHistory (子组件)                        │
│                                                 │
│  useChatWithHistoryContext() 获取数据           │
└─────────────────────────────────────────────────┘
                        │
          ┌─────────────┼─────────────┐
          ▼             ▼             ▼
     ┌─────────┐  ┌─────────┐  ┌─────────┐
     │ Sidebar │  │ Header  │  │ ChatWrap│
     └─────────┘  └─────────┘  └─────────┘
          │             │             │
          └─────────────┴─────────────┘
                        │
              孙组件同样可以通过
           useChatWithHistoryContext() 获取数据

八、最佳实践

  1. 小型项目:使用原生 React Context 即可,简单直接
  2. 大型项目:当 Context 字段较多(10+)时,考虑使用 use-context-selector
  3. 拆分 Context:如果可能,将不相关的数据拆分到不同的 Context 中
  4. 命名规范:导出一个自定义 hook(如 useMyContext),统一消费方式

九、总结

场景 推荐方案
简单数据共享 React 原生 Context
大型 Context,字段多 use-context-selector
Vue 背景开发者 理解为 provide/inject 的 React 版本

Context 本质上就是 跨层级传递数据 的工具,理解了这一点,无论是 React 还是 Vue,核心概念都是相通的。

不写 Canvas 也能搞定!小程序图片导出的 WebView 通信方案

作者 王嗨皮
2026年3月3日 14:06

大家好,我是王嗨皮,一名主业前端,副业全栈的程序员,如果我的文章能让您有所收获,欢迎一键三连(评论,点赞,关注),感谢!

年前业务部门的同事提了一个需求,将公司PC端询价系统的报价单导出功能移植到到小程序上。

最初接到这个任务时,有点小崩溃,主要问题有两个:

  1. 小程序无法操作DOM元素,因此不能使用 html2Canvas 像PC端一样直接将DOM元素生成图片。
  2. 如果用Canvas自己画,只能手写大量代码,可读性差,拓展困难。

在对着uni-app文档思考了一段时间之后,决定尝试一下小程序与webview双向通信这个解决方案。

解决思路

其实思路也不复杂,就是利用 uni-app 的 <webview> 组件嵌入一个部署在服务器上的 H5 页面,借助小程序与 H5 之间的通信机制,将图片生成的工作转移到 H5 端完成,然后将生成的 base64 图片文件返回给小程序并保存到本地相册。

Screenshot 2026-03-03 at 09.31.35.png

完整方案代码

简单说一下这个方案的优势:

  1. 解决了小程序的DOM限制:H5的环境下可以操作DOM,使用 html2canvas 可以正常运行。
  2. 可读性/拓展性强:页面直接用传统的三件套(HTML/CSS/JS)构建,容易理解。同时针对不同业务板块可以拓展多个模板。
  3. 职责分离:小程序只负责传递数据,H5负责渲染页面和截图。

当然,在这个过程中我也需要和后端同事提前做好沟通,H5页面的数据是需要通过接口传参获取的。

小程序端代码:

<template>
  <view class="container">
    <web-view :src="url" @message="handleMessage"></web-view>
  </view>
</template>

<script>
  export default {
    data() {
      return {
        url: '' ,
        isShow: '',
        imgUrl: '',
        priceId: '',
        priceType: '',
        tokenId: '',
      }
    },

    onLoad(options) {
      this.priceId = options.feeId
      this.priceType = options.Type
      this.tokenId = uni.getStorageSync('loginInfo').F_WxToken

      //跳转的url并传递参数
      this.url = 'https://wxa.xxxx.com/index/index/lclindex?priceId=' + this.priceId + '&priceType=' + this.priceType + '&tokenId=' + this.tokenId
    },

    methods: {
      // 保存相册
      savePoster() {
        // 获取用户的当前设置
        uni.getSetting({
          success: (res) => {
            // 验证用户是否授权可以访问相册
            if (res.authSetting['scope.writePhotosAlbum']) {
              this.saveImageToMobilePhotos()
            } else {
              uni.authorize({
                scope: 'scope.writePhotosAlbum',
                success: () => {
                  this.saveImageToMobilePhotos()
                },
                fail: () => {
                  uni.showToast({
                    title: this.$t('pub.author'),
                    icon: 'none',
                    duration: 2000
                  })
                  setTimeout(() => {
                    uni.openSetting({
                      // 调起客户端小程序,让用户开启访问相册
                      success: (res2) => {
                        console.log(res2.authSetting)
                      }
                    })
                  }, 3000)
                }
              })
            }
          }
        })
      },

      // 接收webview传回参数
      handleMessage(e) {
        if(e.detail.data[0].url) {
          this.imgUrl = e.detail.data[0].url
          let base64 = this.imgUrl.replace(/^data:image\/\w+;base64,/, "")
          let filePath = wx.env.USER_DATA_PATH + '/worldjaguar_lclprice.jpg'

          uni.getFileSystemManager().writeFile({
            filePath: filePath,
            data: base64,
            encoding: 'base64',
            success: res => {
              uni.saveImageToPhotosAlbum({
                filePath: filePath,
                success: res2 => {
                  uni.hideLoading()
                  uni.showToast({
                    title: this.$t('pub.saveimageauthor'),
                    icon: "none",
                    duration: 3000
                  })
                },
                fail: err => {
                  uni.hideLoading()
                  console.log(err)
                }
              })
            },
            fail: err => {
              uni.hideLoading()
              console.log(err)
            }
          })
        }
      }
    }
  }
</script>

H5页面代码

在H5页面中兼容小程序并调用uni-app部分API需要分别引入 jweixin.jsuni.webview.js

与小程序端完成信息通信则使用 uni.postMessage

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="content-type" content="application/json; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0, viewport-fit=cover">
    <title>生成报价单</title>
    <link rel="stylesheet" href="./css/common.css">
    <link rel="shortcut icon" type="image/x-icon" href="./favicon.ico" />
  </head>

  <body>
    <div id="app">
      <!-- 添加一层Loading页面遮罩 -->
      <div class="pub-mask" v-show="showMask">
        <div class="pub-mask-box">
          <img src="./images/loading.gif" alt="">
          <span>报价图片生成中...</span>
        </div>
      </div>
      <div class="savebox" id="savebox">
       ......布局代码
      </div>
      <!-- 点击保存图片触发postMessage -->
      <button type="button" @click="postMessage" id="postMessage" class="savebox-image">保存图片</button>
    </div>
    <script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.9/vue.min.js"></script>
    <script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
    <!-- 兼容小程序 -->
    <script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.4.0.js"></script>
    <!-- 必须引入 -->
    <script type="text/javascript" src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script>
    <script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/axios/1.5.0/axios.min.js"></script>
    <script type="text/javascript">
      var App = new Vue({
        el: '#app',
        data: {
          priceId: '',
          priceType: '',
          tokenId: '',
          dataInfo: {},
          userInfo: {},
          freightArr: [],
          polArr: [],
          podArr: [],
          showMask: true,
          dateNumber: ''
        },

        mounted() {
          console.log(document.title)
          this.$nextTick(() => {
            document.addEventListener('UniAppJSBridgeReady', function () {
              uni.getEnv(function (res) {
                console.log('当前环境:' + JSON.stringify(res));
              })
            })
          })
        },

        created() {
          this.priceId = this.getQuery('priceId')? this.getQuery('priceId') : ''
          this.priceType = this.getQuery('priceType')? this.getQuery('priceType') : ''
          this.tokenId = this.getQuery('tokenId')? this.getQuery('tokenId') : ''

          document.title = this.priceType == 1? '生成海出拼箱报价单' : '生成海进拼箱报价单'

          this.getSaveImageData()
        },

        methods: {
          // 接收uni-app小程序传递的参数
          getQuery(name) {
            let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
            let r = window.location.search.substr(1).match(reg);
            if (r != null) {
              // 对参数值进行解码
              return decodeURIComponent(r[2]);
            }
            return null;
          },

          postMessage() {
            html2canvas(document.querySelector('#savebox'), {
              allowTaint: true,
              scale: 2,
              dpi: 300,
              useCORS: true
            }).then(canvas => {
              let imgUrl = canvas.toDataURL('image/jpeg', 1.0)
              // 注意:base64大图可能导致通信超时,如果是海报或高清图片需求建议上传服务器后返回URL
              uni.postMessage({
                data: {
                  url: imgUrl
                }
              })
              uni.navigateBack({
                url: '/pages/saveimage/saveimage‘
              })
            })
          },

          getSaveImageData() {
            axios({
              method: 'post',
              url: 'https://wxa.worldjaguar.com/apis/Lclquote/getLclImgInfo',
              data: {
                QuoteId: 'xxxxxxxx',
                Type: 1,
                ApiType: 2,
                wxAuthorization: 'xxxxxxxx'
              }
            })
            .then(res => {
              if(res.data.code == 200) {
                this.dataInfo = res.data.data
                this.userInfo = res.data.data.Contact

                this.freightArr = res.data.data.freightSurcharge
                this.polArr = res.data.data.departurePortCharges
                this.podArr = res.data.data.destinationPorts

                this.createdPriceNumber()

                setTimeout(() => {
                  this.showMask = false
                }, 2600)
              }else {
                alert(res.data.info)
                this.showMask = false
              }
            })
          }
        }
      })
    </script>
  </body>
</html>

注意事项

<webview> 组件中的 @message 只会在组件销毁页面回退分享时进行触发,不会立刻收到消息,由于我的系统业务逻辑相对简单,所以采用了回退的方式。

比较推荐的做法是将生成的图片上传给服务器接口,然后将生成的URL传递给小程序,这样既能保证同步性,也能解决如果图片过大直接返回小程序造成卡顿的问题。

另外,由于每次生成图片都需要加载一个H5页面, 用户等待时会出现白屏或中间态等影响体验的问题,最好添加一个loading遮罩,优化用户体验。

最后,不要忘记在小程序后台将访问域名配置到白名单,域名确认为HTTPS,确保 <webview> H5页面URL可以正常访问。

QQ截图20231204161819.png

后续思考

这个案例本身并不复杂,但在实际落地过程中,我们仍然经历了近两个小时的讨论与权衡。从技术深度的角度来看,Canvas 方案无疑更能体现开发者的深层技术能力;但在真实的业务场景下,我们更倾向于选择一个可读性强、易于维护、迭代成本低的务实方案。

如果你有更好的方案或建议,欢迎分享交流,感谢🙏!!

🚀 别再乱写 16px 了!CSS 单位体系已经进入“计算时代”,真正的响应式布局

作者 Sailing
2026年3月3日 13:50

如果你的项目里还充满 px + media query —— 那说明你在“维护样式”,而不是“设计系统”。

现代 CSS 的能力,已经远远超出“写几个断点”那么简单。

今天我们从工程视角,把 单位体系 + 计算体系 一次讲透。

u=3751345329,3281070072&fm=253&fmt=auto&app=120&f=JPEG.webp

CSS 单位体系:本质不是长度,而是“依赖关系”

CSS 单位可以分成三类:

  1. 绝对单位
  2. 相对单位
  3. 视口单位

单位能力对比表

单位 类型 依赖对象 系统角色 推荐级别 典型场景
px 绝对 精准控制 ✅ 必须存在 图标 / 1px 边框
em 相对 父级字体 局部缩放 ⚠️ 慎用 组件内部
rem 相对 根字体 全局缩放 ✅✅ 核心 系统布局
vw 视口 视口宽度 宽度适配 响应布局
vh 视口 视口高度 高度控制 全屏模块
vmin 视口 较小值 动态比例 特殊适配
vmax 视口 较大值 极端场景 特殊适配
dvw 动态视口 实际可视宽 移动端修正 🚀 H5
dvh 动态视口 实际可视高 移动端高度修正 🚀 APP/H5

重点认知

单位不是写数值
单位是在声明“依赖谁”

  • px → 不依赖任何人(绝对控制)
  • rem → 依赖根节点(系统级缩放)
  • vw → 依赖视口(设备相关)
  • dvh → 依赖真实可视区域(移动端优化)

如果你做企业级系统:

推荐核心组合

rem + clamp + min/max + dvh

其他单位只是辅助。我们往下看!

WX20240522-152437@2x.png

函数才是现代 CSS 的“计算引擎”

它们不是单位,但它们决定单位如何协作。

函数 作用 优势 工程价值
calc() 计算 混合单位运算 结构关系表达
min() 上限控制 自动封顶 替代 max-width
max() 下限控制 自动兜底 保证可读性
clamp() 区间控制 响应式缩放 替代 media query

核心函数深度解析(实战理解)

现在我们进入“可落地”部分。

🔥 1. rem —— 系统级缩放开关

原理

1rem = htmlfont-size

推荐做法

html {
  font-size: 16px;
}

组件写法:

.box {
  padding: 2rem;
}

为什么它是系统基石?

如果未来:

  • 设计改缩放比例
  • 项目整体要变大

你只需要改:

html { font-size: 18px }

🔥 全站自动缩放。
🔥 无需改任何组件。

这才叫“系统”。

🔥 2. clamp() —— 响应式终极武器

这是现代 CSS 的核心。

语法

clamp(min, preferred, max)

实战:

h1 {
  font-size: clamp(20px, 4vw, 48px);
}

效果:小屏不小于 20px,大屏不超过 48px,中间随视口自动变化。

工程价值

❌ 不需要维护设备列表 media query (@media (max-width: 768px))
❌ 不需要写多个断点
❌ 不需要拆分 PC / Mobile

✅ 一行表达“数学区间关系”

这不是技巧,是范式升级。

🔥 3. min() —— 自动封顶

传统写法:

width: 90%;
max-width: 1200px;

现代写法:

width: min(1200px, 90%);

区别?

  • 数学表达
  • 单行逻辑
  • 结构更清晰

表达的是:

宽度 = 两者中更小的那个

这才叫可维护。

🔥 4. max() —— 自动兜底

.box {
  padding: max(16px, 2vw);
}

保证:

  • 最小 16px
  • 又允许动态放大

用于保证阅读体验、可触控面积。

🔥 5. calc() —— 混合运算核心

.sidebar {
  width: 300px;
}

.content {
  width: calc(100% - 300px);
}

能力:

  • 支持加减乘除
  • 支持单位混合
  • 表达结构关系

它表达的是:

主体宽度 = 容器宽度 - 侧栏宽度

这叫“布局计算”,而不是“写死数值”。

39eb0728a2c0407faacb769863300d59.gif

视口单位:vw / vh / vmin / vmax

它们的共同点只有一个:依赖设备视口

vw

1vw = 视口宽度的 1%
.box {
  width: 50vw;
}

用于横向比例布局。

vh

1vh = 视口高度的 1%
.hero {
  height: 100vh;
}

用于全屏模块。

⚠️ 移动端慎用(dvh 更稳定)。

vmin

vmin = min(vw, vh)

始终基于短边。

.circle {
  width: 50vmin;
  height: 50vmin;
}

横竖屏切换比例稳定。

vmax

vmax = max(vw, vh)

始终基于长边。

.bg {
  font-size: 20vmax;
}

适合视觉冲击型页面。

image(2).png

移动端必须升级:dvh、dvw

移动端地址栏会动态伸缩,100vh ≠ 实际可视高度。

解决方案:

height: 100dvh;

优势:

  • 永远是真实可视区域
  • 不会因浏览器 UI 变化跳动
  • H5 / WebApp 必备

企业级布局推荐方案

如果你做后台系统 / 复杂管理台:

❌ 过时写法

  • 大量 @media
  • 到处 max-width
  • 写死 16px / 20px
  • 用断点区分设备

那是样式堆叠时代。

✅ 现代写法

固定系统:

px + min + max + clamp

弹性系统:

rem + clamp + vw + dvh

目标只有一个:写“关系”,而不是写“数值”。

总结:真正的思维升级

如果你的项目:

  • 还在到处写 16px
  • 还在疯狂加 media query
  • 还在拆 PC / 移动端
@media (max-width: 768px)

你是在:手动划分设备,维护设备列表,增加未来维护成本。

而当你写:

font-size: clamp(16px, 2vw, 24px);

你是在:写数学区间,写系统规则,让浏览器自己计算

你认为呢,希望这篇文章对你有所帮助、有所借鉴,欢迎在评论区随时沟通。

【大白话前端 03】Web 标准与最佳实践

2026年3月3日 13:00

为什么同一套网页代码,在你电脑的 Chrome 上显示完美,发给客户用苹果 Safari 打开,排版就全盘错位?

这并非你写错了代码,而是遇到了前端最底层的现实环境:各大浏览器厂商底层引擎存在差异

Chrome、Safari、Firefox 由不同公司开发,拥有各自的渲染引擎。如果不加限制,同一份代码在不同浏览器里必然会渲染出不同的表现。为了不让开发者被迫写多套代码去兼容不同设备,前端界制定了一套强制性的解析规范,也就是 Web 标准

1. Web 标准到底是什么?

💡 核心定律:标准是给浏览器定的底层规矩,不是面向开发者的编程教程。

很多新手误以为 Web 标准是教人如何写代码的书。其实完全相反,它是用来约束各大浏览器厂商的技术规范。

规范文档里写得极其明确:比如遇到 <button> 标签,浏览器必须默认渲染出一致的预设边距,遇到鼠标悬浮时必须具备固定的交互状态。正因为所有浏览器都必须遵守这套底线规矩,你写下的同一行代码,才能在全世界各种设备上显示一致。

2. 核心三巨头:各自的职责边界

在之前讲解的网页渲染流程中,我们提到浏览器如何解析代码。那么支撑其运转的三大核心技术(HTML、CSS、JS)究竟是如何分工的?

技术名词 核心职责 底层逻辑
HTML 定义内容结构 页面的骨架。仅用标签明确告诉浏览器“这是一个标题”或“这是一个按钮”,绝对不在这里处理排版和颜色。
CSS 控制视觉样式 页面的外观。通过选择器精准找到 HTML 元素,专门负责下达颜色、排版、尺寸等视觉渲染指令。
JavaScript 处理动态交互 页面的逻辑中枢。监听用户的操作(如点击),或向服务器拉取数据,然后动态修改当前的 HTML 结构或 CSS 样式。

3. 野生代码 vs 工业级写法(3 条实战铁律)

理解了浏览器的解析标准后,写代码就不能全凭直觉。业界总结出来的“最佳实践”,本质都是为了让代码结构更清晰、更利于机器解析和后期维护。

铁律一:HTML 语义化(让机器理清重点)

很多人图省事,所有排版全用 <div> 把文本框起来。在外行人看来,只要加了 CSS,这页面长得都一样。但在搜索引擎(如百度)和盲人辅助阅读设备眼里,这就是一堆毫无主次、杂乱无章的纯文本。

⚠️ 常见错误:全篇 div

<!-- 机器完全不知道这两行字的层级关系和具体功能 -->
<div class="large-text">我的博客</div>
<div class="link-btn">进入主页</div>

🛠️ 正确做法:语义化标签

<!-- 机器一秒读懂:h1 是页面唯一核心主题,nav 代表导航区域 -->
<h1>我的博客</h1>
<nav>进入主页</nav>

结论:用正确的标签做正确的事。大标题只用 h1~h6,要点击的交互入口只用 button 或是 <a> 链接。

铁律二:结构、样式、行为三层物理隔离

如果把样式和脚本强行写进 HTML 标签的属性里,叫做“内联写法”。这就相当于把静态结构、视觉表现和业务逻辑死死绑在一起。未来只要业务变大,你想全站统一修改某种按钮的交互或颜色时,只能逐个文件、逐行代码去人工修改,维护成本极高。

⚠️ 常见错误:高重度耦合

<!-- 极其臃肿,且无法被其他页面复用 -->
<button style="color: red;" onclick="alert('购买成功')">购买</button>

🛠️ 正确做法:三层严格分离

<!-- HTML(只管骨架:定义有什么元素) -->
<button id="buy-btn" class="btn-danger">购买</button>

<!-- CSS(只管外观:放在独立的 .css 文件中) -->
.btn-danger { color: red; }

<!-- JS(只管行为:放在独立的 .js 文件中) -->
document.getElementById('buy-btn').addEventListener('click', function() {
  alert('购买成功');
});

铁律三:响应式设计(消灭写死尺寸的恶习)

如今的设备屏幕有几十种物理分辨率。如果在 CSS 里将某个外部容器死死定为 width: 1200px,一旦用户在地铁上用手机打开,页面就会被生硬截断,出现极其难用的底部横向滚动条。

💡 核心定律:用相对单位替换绝对像素。 必须全面转向自适应的相对布局,优先使用百分比(%),配合媒体查询@media),让样式遇到不同设备宽度时能自动适配调整。

🛠️ 正确做法:用媒体查询判断设备宽度

/* 默认手机端小屏幕排版:内容上下堆叠排列 */
.container { 
  display: flex; 
  flex-direction: column; 
}

/* 只要屏幕宽度大于 768px(来到平板或电脑端屏幕),立刻改用横向并排 */
@media (min-width: 768px) {
  .container { flex-direction: row; }
}

📝 总结

无论你用什么框架开发,最终都要回归这 3 条底线规范:

  1. 语义化:用对标签,让机器能读懂层级。
  2. 代码分层:把 HTML、CSS、JS 物理拆分,方便后期维护。
  3. 响应式:放弃写死固定尺寸,用百分比和媒体查询适配所有屏幕。

坚守这三条底线,你的代码在任何浏览器下都能正常显示。


下一章预告: 上述所有的规范和代码拆分,处理的都是页面上能被用户直接看到的区域(即 <body> 里的内容)。 但在实际开发中,很多引发致命报错的问题(比如跨设备乱码、分享到微信时不显示图标、甚至首屏莫名其妙白屏报错)往往不是正文写错了,而是网页的配置出了问题。 下一章请看:【大白话前端 04】HTML 头部的底层逻辑:决定网页解析与检索的隐形配置单,我们将拆解 <head> 这个极易被新手忽略的网页元数据配置区。

如何给 AI Agent 做"断舍离":OpenClaw Session 自动清理实践

作者 AI攻城狮
2026年3月3日 12:53

如何给 AI Agent 做"断舍离":OpenClaw Session 自动清理实践

问题的起源

某天下午,我发现我的 AI 助手越来越"迟钝"——一个简单的问题,从发送到回复,等了将近 86 秒

翻了翻日志,找到了罪魁祸首:上下文长度 122k tokens

这是因为 AI Agent 的每一次对话都会被记录到 session transcript 文件中。随着时间累积,这个文件越来越大,每次推理都要把整个历史塞进模型的上下文窗口,推理时间自然指数级增长。

这就是 长上下文问题(Long Context Problem),是 AI Agent 生产运营中最容易被忽视的性能瓶颈之一。


先搞清楚:Session 和 Transcript 是什么关系?

在 OpenClaw 这类 AI Agent 框架里,session 管理通常分两层:

sessions.json (索引层)
├── agent:main:feishu-xxx  →  sessionFile: /path/to/transcript-abc.json
├── agent:main:feishu-yyy  →  sessionFile: /path/to/transcript-def.json
└── agent:main:main        →  sessionFile: /path/to/transcript-main.json
  • sessions.json — 轻量的"索引文件",记录每个 session 的元数据(创建时间、最后更新时间、对应的文件路径等)
  • transcript 文件 — 实际存储完整对话历史的 JSON 文件,每条消息、每次 tool call、每个 token 都在这里

关键点:这两个东西是独立存在的。

如果你只删了 sessions.json 里的 key(索引),而没有删对应的 transcript 文件(数据),会发生什么?

  • 下次启动时,框架找不到这个 session 的索引,会重新创建
  • 但旧的 transcript 文件还静静躺在磁盘上,永远不会被清理
  • 随着时间推移,磁盘上堆满了孤儿文件,慢慢侵蚀存储空间

这就是为什么清理要同时处理 key 和 file,缺一不可。


解决方案:自动化清理脚本

我写了一个 Bash + Node.js 混合脚本来处理这个问题:

#!/usr/bin/env bash
# 清理 Feishu session + Main session 脚本

set -e

SESSIONS_FILE="/home/water/.openclaw/agents/main/sessions/sessions.json"
THRESHOLD_MS=$((24 * 60 * 60 * 1000))  # 24 小时阈值

核心逻辑(Node.js 内嵌)

const data = JSON.parse(fs.readFileSync(SESSIONS_FILE, 'utf8'));
const now = Date.now();

Object.keys(data).forEach(k => {
  if (!k.includes('feishu')) return;  // 只处理 feishu sessions
  if (k.includes('cron')) return;      // 跳过 cron sessions

  const session = data[k];
  const updatedAt = session.updatedAt || session.createdAt || 0;
  const age = now - updatedAt;

  if (age > threshold) {
    // ① 先删文件
    const sessionFile = session.sessionFile;
    if (sessionFile && fs.existsSync(sessionFile)) {
      fs.unlinkSync(sessionFile);
    }
    // ② 再删索引
    delete data[k];
    deleted++;
  }
});

// 写回索引文件
fs.writeFileSync(SESSIONS_FILE, JSON.stringify(data, null, 2));

注意操作顺序:先删文件,再删索引。 反过来的话,如果删完索引时进程崩溃,文件就变成永久孤儿了。

特殊处理:Main Session 每日强制重置

除了 Feishu session,我还对 main session 做了无条件的每日清理:

const mainKey = 'agent:main:main';
if (data[mainKey]) {
  const sessionFile = data[mainKey].sessionFile;
  if (sessionFile && fs.existsSync(sessionFile)) {
    fs.unlinkSync(sessionFile);
  }
  delete data[mainKey];
}

这里不判断年龄,直接删。理由是:

  • Main session 是日常对话的主 session,积累最快
  • 每天重置一次,保持上下文干净,推理速度稳定
  • 重要信息通过 MEMORY.md 持久化,不依赖对话历史

重启 Gateway

清理完 sessions.json 后,需要重启 Gateway 让变更生效:

pkill -f openclaw-gateway || true
sleep 2
nohup openclaw-gateway >> "$LOG_FILE" 2>&1 &
sleep 3

这里用 || true 避免 pkill 找不到进程时退出码非零触发 set -e


自动化:配置 Cron Job

把这个脚本配成每天定时跑,完全不用人工介入:

{
  "name": "Daily Feishu Session Cleanup",
  "schedule": { "kind": "cron", "expr": "0 14 * * *", "tz": "Asia/Shanghai" },
  "sessionTarget": "isolated",
  "payload": {
    "kind": "agentTurn",
    "message": "Run the Feishu session cleanup script and report results",
    "timeoutSeconds": 120
  },
  "delivery": { "mode": "announce", "channel": "feishu" }
}

几个设计要点:

  1. sessionTarget: isolated — 在隔离 session 里跑,不污染主 session
  2. kind: agentTurn — 让 Agent 执行脚本并汇总结果,通过 Feishu 推送
  3. 每天 14:00 — 下午低峰期执行,避免影响正常使用

⚠️ 坑点:sessionTarget: main 只支持 payload.kind = systemEvent(直接执行,无 LLM)。需要 LLM 汇报 + 推送通知,必须用 isolated + agentTurn,混用会超时或无推送。


效果对比

指标 清理前 清理后
上下文长度 ~122k tokens < 5k tokens
平均响应时间 21-86 秒 3-8 秒
磁盘占用(sessions) 持续增长 每日重置

响应时间从最差 86 秒降回 3-8 秒,体感差别非常明显。


延伸思考:AI Agent 的"记忆管理"

这个问题本质上是 AI Agent 的**工作记忆(Working Memory)vs 长期记忆(Long-term Memory)**的分离问题。

  • 对话 session / transcript = 工作记忆,应该短暂且聚焦
  • MEMORY.md / 知识库 = 长期记忆,存真正重要的决策和知识

很多人在部署 AI Agent 时,会默认让它"记住一切",结果把工作记忆当成了永久存储,导致上下文爆炸。

正确的做法是:

  1. 定期蒸馏——把对话中有价值的信息提炼写入长期记忆
  2. 定期清理——工作记忆不需要无限堆积
  3. 分层存储——工作 session、日志、知识库各司其职

这和人类的记忆机制其实很像——你不会把每天说过的每句话都记着,但重要的决定、学到的知识会留下来。


总结

一个简单的 cron 清理脚本,解决了 AI Agent 最常见的性能退化问题。关键点:

  • 同时删 key 和 file,避免磁盘泄漏
  • 先删文件再删索引,保证原子性
  • Main session 每日强制重置,保持上下文干净
  • 自动化 + 通知,完全无人值守

如果你也在跑 AI Agent,不妨检查一下你的 session 文件有多大——也许已经悄悄堆了几十 MB 的历史对话了 😄

Node.js 拓展

2026年3月3日 12:44

1. Node.js概览

image.png

2. 尝试node.js

2.1 nodemon-一个自动执行的插件

如果文件内容有变更,则需要再在终端中执行‘node 目标文件’,此时可以使用nodemon插件。 命令:node 目标文件 image.png安装完成之后,每次代码内容变更,就可以自动重新执行 命令: nodemon 目标文件

image.png

2.2 使用node搭建web服务器

// 使用node.js内置的http模块创建web服务器
const http=require('http');
// 使用createServer创建web服务器实例
const server= http.createServer((req,res)=>{
  // 使用res.end进行响应内容的设置
res.end('hello world')
})
// 使用server.listen方法监听端口3000
server.listen(3000,()=>{
    console.log('server is running at http://127.0.0.1:3000')
})

image.png

程序员必看:软著不是“面子工程”,是代码的“法律保险”

2026年1月30日 14:30

作为一名敲了8年代码的程序员,我见过太多同行的无奈:熬夜3个月打磨的核心算法被竞品“照搬”,却因没有软著维权无门;创业做产品,想上架应用市场却卡在软著申请,自己写的材料因“技术描述不规范”被驳回;甚至有同事离职后,把公司未登记的代码稍作修改就当成自己的成果——这些教训,再加上我亲身经历和见证的真实案例,让我深刻意识到:软著不是企业的“面子工程”,而是程序员劳动成果的“法律保险”,更是职业路上的“硬通货”。

今天就从技术人的角度,结合真实案例科普软著的核心知识点,再分享一套高效申请方案,帮同行们少走弯路,让自己的代码真正得到保护。

先科普:程序员必懂的3个软著核心问题(附真实案例)

1. 软著保护的是“代码”还是“思路”?看了这个维权案例就懂

很多程序员有个误区:“我的算法是独创的,软著能保护吗?”其实不然——软著保护的是代码的“表达形式”,而非底层算法、逻辑思路或功能需求。简单说,别人不能直接复制你的源代码、照搬你的软件界面布局和操作流程,但如果他用不同的代码实现了相同的功能,并不构成侵权。

这里分享一个最高法公布的典型案例:苏州某网络科技公司投入2589万元研发的“OfficeTen”网关系统,取得软著后,被两名离职工程师带着源代码跳槽到竞品公司,复制修改后用于同类产品销售,抢占原公司客户。经鉴定,被诉软件与涉案软件非开源源代码相同率高达90.2%。最终,法院凭借软著证书和鉴定报告,认定侵权成立,判令对方停止侵权并赔偿损失。

这个案例很有代表性:如果没有软著,原公司很难证明代码归属,维权会陷入“举证难”的困境;而有了软著,再结合代码比对鉴定,就能清晰界定侵权行为。这也印证了软著的核心价值——它是代码归属权的“官方凭证”,更是维权的“核心武器”。

2. 为什么自己写的材料总被驳回?这些踩坑案例太真实

这是程序员申请软著最常踩的坑,本质是我们擅长写逻辑严谨的代码,却对版权局的“文书规范”不敏感。结合我身边和行业内的案例,常见驳回原因主要有3类:

案例一:源代码不合规被驳回。深圳某初创公司的技术负责人,首次提交软著材料时,直接上传了完整代码库,既没按要求截取前30页+后30页,还混入了第三方开源代码片段,导致被查重驳回。更无奈的是,他手动修改时,又因每页代码行数不足50行、注释占比超标,二次驳回。

案例二:操作手册太“口语化”被驳回。我前同事开发了一款健身社交APP,写操作手册时只描述“用户点击登录按钮即可登录”,既没有系统架构图、功能流程图,也没标注核心模块的技术实现逻辑,被版权局以“技术描述不清晰”驳回,反复修改了2周仍未达标。

案例三:版本一致性问题被驳回。杭州某智能硬件公司,提交的材料中软件版本号是V2.0,但源代码实际是V1.8版本,功能模块描述与代码实现不一致,直接被判定为“材料与实物不符”,耽误了高新企业申报进度。

这些案例的共性的是:我们习惯用技术思维写材料,却忽略了软著申请的核心要求——“规范、精准、可验证”,反复修改既浪费时间,又容易泄露核心代码片段。

3. 软著对程序员有啥实际好处?这些案例比“画饼”更实在

除了企业层面的政策红利,对程序员个人而言,软著绝不是“废纸一张”,而是职业发展的“隐形加分项”:

案例一:软著助力晋升加薪。我前团队的小王,去年参与公司核心项目开发后,主动申请了2项软著。年度晋升评审时,他不仅展示了项目成果,还拿出软著证书证明自己的技术贡献,最终在同级工程师中脱颖而出,成功晋升高级工程师,薪资涨幅20%。对技术人来说,软著能把抽象的“技术能力”转化为具体的“知识产权成果”,比空口说“我技术好”更有说服力。

案例二:软著成为求职“敲门砖”。我朋友去年跳槽时,简历上标注了“独立开发XX系统,持有软著证书”,直接通过了大厂初筛。面试时,面试官围绕软著对应的项目提问,他凭借清晰的技术逻辑和成果证明,顺利拿到offer。还有高校毕业生凭借自研校园管理系统的软著,直接斩获名企算法岗offer,起薪翻倍。

案例三:软著规避离职纠纷。我同行李工程师,离职前开发的一款财务管理工具,提前申请了软著并明确了归属权。离职后,原公司以“代码是在职期间开发”为由要求他移交核心算法,他凭借软著证书和开发协议,清晰界定了个人成果与公司成果的边界,避免了不必要的扯皮。

技术人专属:高效申请软著,专注写代码不费心

结合前面的案例痛点,我们的核心精力应该放在研发上,而非纠结材料格式规范。分享一套我亲测有效的高效申请流程,帮同行们避开材料驳回的坑,节省时间成本,同时保障代码安全:

  1.  规范准备源代码:按版权局要求,截取软件源代码前30页+后30页,确保每页代码行数不少于50行,注释占比不超过30%,剔除第三方开源代码片段,提前自查原创性,避免查重驳回。

  2.  精准撰写技术文档:避免口语化描述,文档中需包含系统架构图、功能流程图,清晰标注核心模块的技术实现逻辑(如采用的技术框架、核心算法、模块调用关系等),确保技术描述精准、可验证,贴合审核要求。

  3.  校验版本一致性:仔细核对材料中软件版本号、功能模块描述,确保与实际源代码、软件实物完全一致,避免因版本不符被驳回。

  4.  规范提交流程:登录中国版权保护中心官网,按指引上传准备好的源代码、技术文档、申请表等材料,核对无误后提交,后续及时关注审核进度,若需补正,针对性优化材料即可。

最后提醒:代码要保护,软著要趁早

作为程序员,我们敲的每一行代码都是心血结晶,不能让它“裸奔”。从苏州公司的维权案例,到小王的晋升案例,再到无数次材料驳回的教训,都在印证一个道理:软著申请没有技术门槛,难的是吃透规范、节省时间。

掌握正确的申请方法,就能避开大部分坑,让软著真正成为代码的“法律保险”。把时间花在打磨代码、提升技术上,同时给每一份技术成果做好保护——这才是技术人该有的效率,也是对自己劳动成果最基本的尊重。

AI辅助开发的基础概念

作者 牛奶
2026年3月2日 23:55

AI辅助开发的基础概念

这是系列第二篇,上一篇聊完"为什么学",我们来看看 AI 辅助开发到底帮我们做了什么,需要掌握哪些概念,以及怎么用好 AI。


上一篇文章,我们聊了为什么前端必须学AI。

简单说就是:AI不会取代前端,但它会重新定义前端。

既然要学,接下来一个很自然的问题就是:到底学什么?怎么学?

市面上关于AI的资料,多的让人头皮发麻。

一会儿有人说要学Prompt工程,一会儿有人说要学LangChain,一会儿又有人说要学向量数据库。一顿操作下来,还没开始就已经晕了。

这篇文章,我帮你把AI辅助开发这件事 全面梳理一遍。不求深,但求全。让你知道整个图景是什么样的,遇到什么该学什么。


原文地址

墨渊书肆/AI辅助开发的基础概念


先理解三个问题

在开始之前,我们先回答三个最基本的问题:

  1. AI能帮我做什么? — 知道它的能力边界
  2. 需要掌握哪些概念? — 理解背后的原理
  3. 怎么用好AI? — 具体的操作方法

这三个问题对应AI辅助开发的三个层次。接下来我们一层一层来讲。


第一层:AI能帮你做什么

这是最实在的东西——AI到底能帮我们干什么活。

1. 写代码

根据你的描述生成代码。

"帮我写一个React组件,功能是:用户可以选择日期范围,支持禁用特定日期,暗色模式适配"

AI能帮你写:

  • 完整的组件(React、Vue)
  • 工具函数
  • API接口
  • 样式代码(CSS、Tailwind)
  • TypeScript类型定义
  • 测试用例

2. 读代码

帮你理解你不懂的代码。

"这个组件的数据流是怎么走的?为什么要用useMemo?"

AI能帮你:

  • 解释代码逻辑
  • 梳理复杂代码
  • 找出潜在问题
  • 理解项目结构

3. 查文档

以前遇到问题,你先去 Google 搜,然后看 Stack Overflow,最后才去翻文档。

现在直接问AI:

"Next.js 15怎么做密码重置?"
"Vercel AI SDK怎么实现流式响应?"

AI直接从文档里给你准确的答案。

4. 修Bug

遇到报错了,直接问AI:

"这个报错是什么意思?TypeError: Cannot read properties of undefined (reading 'map')"

AI会告诉你:错误原因是什么、最可能出在哪个地方、怎么修复。

5. 代码Review

让AI帮你审查代码:

"请审查以下代码,指出:1. 潜在安全问题 2. 性能问题 3. 代码规范问题"

它会从多个角度帮你分析一遍。

6. 重构代码

觉得某段代码写得烂,但不知道怎么改?

"请帮我重构以下代码,要求:1. 使用TypeScript类型 2. 提取可复用逻辑 3. 增加错误处理"

AI会给一个全新的版本,你可以参考它的思路。

7. 写测试

写测试很枯燥,但很重要。

"请为以下函数编写单元测试,覆盖:正常情况、空输入、错误输入"

AI生成测试代码,你再根据需要调整。

8. 帮你想名字

我经常让AI帮我给变量、函数起名字。

"我有一个函数,接受用户ID,返回用户名、邮箱、头像、最后登录时间。请帮我想一个合适的函数名"

AI会给三四个建议,通常都比较规范,符合命名习惯。


第二层:需要掌握哪些概念

知道AI能帮你做什么了,接下来你需要理解一些核心概念。这样你才能更好地和AI配合。

1. 大模型(LLM)

LLM,Large Language Model,大语言模型。

你可以把大模型理解成一个见过海量代码的超级程序员。它看过互联网上几乎所有的开源代码、文档、教程,所以它知道你想要什么。

GPTClaudeDeepSeek通义——这些都是大模型。

它不是真的"智能",而是见过太多了,知道概率最高的答案是什么

作为前端开发者,你不需要会训练模型,你只需要知道怎么调用它们

2. Token

Token 是 AI 处理信息的基本单位

简单理解:AI不是按"字"或"词"来处理文字的,而是按 Token。一个Token可能是半个单词、一个单词、或者一个标点符号。

为什么这个概念重要?因为:

  • API按Token收费:输入和输出都算 Token
  • 上下文长度限制:你给 AI 的上下文不能超过它能处理的Token数

举个例子:Claude 3.5 支持 200K Token 的上下文,意味着你可以一次性把整个项目的代码都丢给它(但这样做很贵,而且AI可能会"忘记"中间部分)。

3. Agent

Agent(智能体)是AI领域最重要的概念之一。

你可以把Agent理解成一个能自己执行任务的AI。不像普通的聊天AI只能"问一句答一句",Agent可以:

  • 自己规划步骤
  • 自己调用工具
  • 自己检查结果
  • 自主完成复杂任务

这就是为什么很多人说"AI不是替代程序员,而是替代不会用AI的程序员"——因为Agent已经可以自主完成很多开发任务了。

4. MCP(Model Context Protocol)

MCP是这两年AI辅助开发领域最重要的新东西。

你可以把它理解成AI的"USB接口"

以前AI只能跟你聊天,它不知道你电脑里有什么、你的项目是什么样子。MCP出现后,AI可以:

  • 读取你电脑上的文件
  • 执行终端命令
  • 访问你的代码仓库
  • 调用各种第三方服务

这就是为什么现在的AI编程工具突然变得超级强大——因为它们不只是"聊天"了,它们真的能"干活"了。

5. Prompt

Prompt就是你给AI说的话。「帮我写个登录功能」就是一个Prompt。

Prompt不是越长越好,而是越精确越好

好的Prompt应该包含:

  • 上下文:你用的技术栈是什么
  • 具体需求:你想要什么功能
  • 约束条件:有什么特殊要求
  • 期望结果:你想要的输出格式

6. 幻觉

AI有时候会"编故事"——生成一些看似正确但实际上是错误的内容。这就是所谓的"幻觉"

为什么会产生幻觉?因为AI本质上是在"预测"下一个最可能的词,而不是真的"理解"事实。

这意味着:

  • AI给出的答案不一定是对的
  • 你要有能力判断答案对不对
  • 重要的代码要自己验证

第三层:怎么用好AI辅助开发

知道能做什么、也理解概念了,接下来就是具体怎么用。

1. 搞清楚什么时候用什么模式

大部分AI编程工具都有两种模式:

对话模式(Chat):你问一句,它答一句。

我一般用来:

  • 问具体问题:「这个报错是什么意思?」
  • 查知识点:「PostgreSQL的索引类型有哪些?」
  • 解释代码:「这个函数做了什么?」

任务模式(Agent):你描述一个任务,它自己去分析和执行。

我一般用来:

  • 帮我重构整个模块:「把这个登录从JWT改成Session」
  • 帮我修bug:「登录一直返回401,帮我看看是什么原因」
  • 帮我实现一个功能:「帮我实现用户注册功能,包含表单验证、数据库存储」

简单说:小问题用对话,大任务用Agent。

2. 喂上下文是有技巧的

AI最强的地方是它能理解你的整个项目。但有时候它也会犯傻——给你一些牛头不对马嘴的回答。

这时候,你得学会喂上下文

我犯过的错误:

「怎么优化这个查询?」

AI回了半天,什么加索引、分页、缓存讲了一套,我根本不知道它说的是什么,因为我连我的表结构都没告诉它。

后来我学乖了:

「我的Prisma查询是这样的:prisma const users = await prisma.user.findMany() 数据量大概10万条,现在查询要3秒,请问怎么优化?」

这次AI直接告诉我:1. 加索引 2. 用select只查需要的字段 3. 考虑分页。

我的习惯是:至少告诉AI三件事

  1. 技术栈:我用的技术是什么(Next.js + Prisma + PostgreSQL)
  2. 当前代码:现在代码长什么样(贴上代码)
  3. 问题:遇到了什么问题(查询慢、报错、不知道怎么做)

3. 选中代码让AI帮你改

选中一段代码,让AI帮你修改。这是一个核心技巧。

比如我选中一个函数,这样用:

「请帮我添加错误处理和类型定义」

AI直接在原代码基础上帮我改好了,我只需要确认一下就行。

比让它生成一段新代码然后我再替换,效率高很多。

再举几个我常用的场景:

  • 选中一段面条式代码:「请帮我重构这段代码」
  • 选中一个API接口:「请帮我添加参数校验」
  • 选中一个组件:「请把这个组件改成响应式」

4. 用@引用代替复制粘贴

大部分AI编程工具支持用@符号引用特定内容:

  • @File :引用当前打开的文件
  • @components/UserCard.tsx :直接引用某个文件
  • @Folder :引用整个文件夹
  • @Docs :引用官方文档
  • @Search :搜索项目内的代码

最常用的场景:

@components/UserCard.tsx 请帮我在这个组件里添加一个编辑用户信息的功能

AI直接读取文件内容,在正确的位置帮我添加代码。

@Docs 请帮我查一下Next.js的metadata怎么用来做SEO

AI直接读官方文档,给我准确的答案。

用@引用比复制粘贴代码更省Token,而且AI能更准确地理解你的需求。

5. 一次性把需求说清楚

让AI一次性完成所有需求,别分开问:

  • ❌ 「先帮我写HTML」「再帮我写样式」「再加个交互」
  • ✅ 「帮我写一个登录组件,包含表单验证、错误提示、暗色模式支持」

一次说清楚,AI能更好地理解你的整体需求,生成的代码也更连贯。

6. 设置好项目规范

我在每个项目都会设置规范文件。

设置好之后,AI每次生成代码都会自动遵循这些规范。

举个例子:我不用每次都说「API错误要返回success和error字段」,AI自己就知道。

规范内容包括:

  • 技术栈和版本
  • 目录结构
  • 代码规范(命名、格式)
  • 常用的工具函数

7. 积累自己的Prompt模板

AI 的输出质量很大程度上取决于你的 Prompt,好的 Prompt 能让AI 生成更准确、更符合你需求的代码。

在平时的开发中,可以把一些常用的场景总结成模板,方便后续快速调用。


工作流程:AI怎么融入日常开发

知道有什么工具了,接下来我们看看AI是怎么融入日常开发工作的。

我总结了一个我自己常用的工作流程:

1. 需求分析阶段

  • 解释需求文档里的技术术语
  • 给出技术选型的建议
  • 评估实现难度和时间

2. 编码实现阶段

  • 根据描述生成代码
  • 解释你不熟悉的API
  • 帮你写测试用例
  • 优化现有代码

3. 代码审查阶段

  • 检查代码安全性
  • 找出潜在性能问题
  • 审查代码规范
  • 解释复杂逻辑

4. Bug修复阶段

  • 分析报错信息
  • 定位问题原因
  • 给出修复建议

5. 文档输出阶段

  • 从代码生成注释
  • 生成README文档
  • 写API文档

写在最后

这篇文章帮你把AI辅助开发这件事,从概念到工具,从流程到趋势,全面梳理了一遍。

你不需要记住所有细节,你只需要知道:

  1. AI能帮你做什么:写代码、读代码、查文档、修Bug、代码Review、重构、写测试
  2. 需要理解什么概念:大模型、Token、Agent、MCP、Prompt、幻觉
  3. 怎么用好AI:喂上下文、选对模式、用@引用、一次性说清楚需求

知道了能做什么、懂了概念,剩下的就是多练习。

AI辅助开发不是玄学,就是一个工具。用多了,你会发现:会问问题的人,效率比不会问的人高十倍。


篇预告

下一篇,我会讲讲《2026年大模型怎么选?前端人实用对比》。

市面上有那么多大模型,GPT、Claude、DeepSeek、通义千问......到底该用哪个?不同的场景该怎么选?怎么才能低成本使用?

我会从实际开发的角度,给你一个清晰的选型建议。

感兴趣的话,下一篇见。

自定义右键菜单:在项目里实现“选中文字即刻生成新提示”

2026年3月3日 06:42

一个真正丝滑的项目,交互不能只停留在点击按钮。 “选中即触发” (Selection-to-Action)是生产力工具的标配。

实现这个功能看似简单,实则藏着不少关于 Selection API视口坐标计算的深坑。


1. 核心流程:监听、获取、定位

第一步:捕捉用户的“选中时刻”

虽然有 selectionchange 事件,但在做浮窗时,mouseup 通常更稳,因为它能确保是在用户完成拖拽动作后触发。

JavaScript

document.addEventListener('mouseup', handleSelection);

function handleSelection(e) {
  const selection = window.getSelection();
  const selectedText = selection.toString().trim();

  if (selectedText.length > 0) {
    const range = selection.getRangeAt(0);
    // 关键点:获取选中文字在视口中的精确几何位置
    const rect = range.getBoundingClientRect();
    
    showFloatingMenu(rect, selectedText);
  } else {
    hideFloatingMenu();
  }
}

2. 坐标计算:让浮窗“如影随形”

这是最容易翻车的地方。getBoundingClientRect() 返回的是相对于**视口(Viewport)**的坐标。如果你的页面有滚动条,或者容器是 position: relative,直接赋值 top/left 会让浮窗飞到九霄云外。

正确的绝对定位公式:

Left=rect.left+window.scrollX+(rect.width/2)(menuWidth/2)Left = rect.left + window.scrollX + (rect.width / 2) - (menuWidth / 2)

Top=rect.top+window.scrollYmenuHeightoffsetTop = rect.top + window.scrollY - menuHeight - offset

JavaScript

function showFloatingMenu(rect, text) {
  const menu = document.getElementById('floating-menu');
  const offset = 10; // 距离文字上方的间距

  // 计算位置:居中显示在选中文字上方
  const left = rect.left + window.scrollX + (rect.width / 2);
  const top = rect.top + window.scrollY - offset;

  Object.assign(menu.style, {
    display: 'flex',
    left: `${left}px`,
    top: `${top}px`,
    transform: 'translate(-50%, -100%)' // 利用 transform 实现水平对齐
  });

  menu.dataset.selectedText = text; // 暂存文字供后续使用
}

3. 避坑指南

① 避免“点一下就弹”

如果用户只是单纯点击了一下(没有选中任何字),mouseup 也会触发。

  • 解决:除了判断 selectedText.length > 0,还可以记录 mousedown 的位置,如果 mouseup 的位置没变,说明是点击而非选择。

② 浮窗点击冲突:onmousedown 陷阱

当你点击浮窗上的“复制”按钮时,浏览器默认会清除当前页面的文字选中状态,导致 mouseup 再次触发把浮窗关掉。

  • 解决:在浮窗容器上使用 onmousedown={(e) => e.preventDefault()}。这样点击浮窗时,焦点不会离开原来的文字。

③ 边界检测(Viewport Boundary)

如果选中的文字在屏幕最顶端,浮窗会超出屏幕。

  • 对策:判断 rect.top 是否小于浮窗高度。如果是,则将浮窗显示在文字下方

4. 功能实现:一键复制与翻译

JavaScript

// 复制逻辑(复用我们上一篇文中的 safeCopy)
menu.querySelector('.copy-btn').onclick = async () => {
  const text = menu.dataset.selectedText;
  await safeCopy(text);
  showToast('已复制!');
  hideFloatingMenu();
};

// 翻译逻辑:调用 AI 接口
menu.querySelector('.translate-btn').onclick = async () => {
  const text = menu.dataset.selectedText;
  // 直接跳转到 AI 对话框并自动输入 Prompt
  router.push(`/chat?prompt=请翻译以下文字:${text}`);
};

5. 交互进阶:移动端长按适配

在移动端,用户习惯长按选择文字。

  • 方案:现代移动浏览器会自动弹出系统菜单。如果你想覆盖它,需要监听 contextmenu 事件,或者通过 CSS 属性 -webkit-touch-callout: none; 禁用系统菜单,再手写一套长按逻辑(touchstart + setTimeout)。

告别后端转换:高质量批量导出实战

2026年3月3日 06:41

对于 100 条这种量级的数据,虽然内存压力不大,但格式的标准化导出的顺滑度是区分“初级实现”和“资深工具”的关键。

在应用场景下,用户导出数据通常有两个目的:备份/迁移(JSON)或分享/离线审阅(Excel)


1. 方案选择:JSON vs. XLSX

格式 优势 劣势 资深开发建议
JSON 结构化、无损、浏览器原生支持。 对非技术人员不友好。 用于系统备份、数据迁移。
XLSX 可读性极强、支持排序筛选。 需要第三方库、体积较大。 用于周报汇报、团队分享。

2. 导出 JSON:最纯粹的无损备份

这是最简单的方案,不需要任何库,直接利用 BlobURL.createObjectURL

JavaScript

/**
 * 导出为 JSON 文件
 * @param {Array} data - Prompt 数组
 */
function exportToJSON(data) {
  // 1. 序列化,添加 2 个空格缩进方便阅读
  const content = JSON.stringify(data, null, 2);
  
  // 2. 创建 Blob 
  const blob = new Blob([content], { type: 'application/json' });
  
  // 3. 触发下载
  downloadFile(blob, `Prompts_Backup_${new Date().getTime()}.json`);
}

3. 导出 XLSX:专业的表格方案

对于 Excel 导出,SheetJS (xlsx) 是行业标准。作为 8 年老兵,我会建议你使用 动态引入(Dynamic Import) ,因为这个库体积较大(数百 KB),没必要在首屏加载。

实战代码(按需加载型)

JavaScript

async function exportToXLSX(data) {
  // 1. 动态加载 SheetJS
  const XLSX = await import('https://cdn.sheetjs.com/xlsx-0.20.1/package/dist/xlsx.full.min.js');

  // 2. 数据扁平化(如果你的 Prompt 包含嵌套对象,需要先处理)
  const worksheet = XLSX.utils.json_to_sheet(data);
  
  // 3. 创建工作簿
  const workbook = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(workbook, worksheet, "MyPrompts");

  // 4. 写入并触发下载
  XLSX.writeFile(workbook, `Prompt_Export_${new Date().toLocaleDateString()}.xlsx`);
}

4. 资深开发的“性能与健壮性”补丁

① 统一的下载触发器(避免内存泄漏)

频繁导出时,如果不销毁 ObjectURL,会导致页面内存占用持续攀升。

JavaScript

function downloadFile(blob, fileName) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = fileName;
  document.body.appendChild(a);
  a.click();
  
  // 关键:延迟移除和销毁,确保下载任务已交给浏览器
  setTimeout(() => {
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }, 100);
}

② 数据清洗(避免 Excel 报错)

Excel 对某些特殊字符或过长的单个单元格(超过 32767 字符)会报错。

  • 对策:在 json_to_sheet 之前,遍历数据,对超长 Prompt 进行截断或分块,或者至少给用户一个提示。

③ 导出时的“防抖”与“状态反馈”

100 条数据虽然快,但如果是 10000 条,UI 可能会卡死。

  • 对策:点击导出后,按钮立即进入 Loading 状态,并使用 Web Worker 处理数据序列化,最后再回到主线程触发下载。

5. 加分项

  1. 文件名命名规范:不要只叫 data.json。推荐 [应用名]_[分类]_[日期].xlsx
  2. 表头国际化:如果你的工具面向国际,导出的 Excel 表头(如:标题、内容、创建时间)应该根据当前 UI 语言动态映射。
  3. CSV 降级:如果不想引几十 KB 的 xlsx 库,且数据结构简单,导出 CSV 是最高性能的方案。但要注意加上 BOM (Byte Order Mark) 头(\ufeff),否则 Excel 打开中文会乱码。

JavaScript

// CSV 乱码修正技巧
const csvContent = "\ufeff" + convertToCSV(data);
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });

弃用html2pdf.js,这个html转pdf方案能力是它的几十倍

作者 刘发财
2026年3月3日 02:06

欢迎转载文章

在前端开发中,“把网页变成 PDF”是个老生常谈的需求。无论是生成发票、报告还是简历,用户总希望点一下按钮就能带走一份格式完美的文档。 目前主流的前端html转pdf方案是通过html2canvas将网页渲染成canvas,再通过jsPDF将canvas转换为pdf。代表方案就是 html2pdf.js,npm包周下载量达到了80万,为广大开发者所接受。但是因为它基于html2canvas和jsPDF,会有一些无法解决的问题,比如:

  • 生成速度慢
  • 生成的pdf文件体积大
  • 生成的pdf内容会模糊,打印时无法达到清晰度要求
  • 文字无法被搜索,选中,编辑,因为它生成的pdf是图片式的,而非矢量pdf

而现在,有一种全新的解决思路,完美的解决了这些问题,那就是作者开源的前端pdf生成库dompdf.js,具体的实现和说明可以查看我上一篇文章 https://juejin.cn/post/7583912637470769203

在线体验

dompdfjs.lisky.com.cn

Git 仓库地址 (欢迎 Star⭐⭐⭐)

github.com/lmn1919/dom…

gitee.com/liu-facai/d…

dompdf.js的大致原理

1.解析 html 页面,生成一个包含节点位置信息,样式,层级,内容等信息的 DOM 树。

2.递归 DOM 树,根据节点据顶部的高度和生成页面规格的高度,将节点分配到不同的页面。

3.调用 jspdf.js 的 api,将节点绘制到 PDF 文件上。

可以看出,dompdf.js 跳过了html转图片的步骤,直接将 DOM 树转换为矢量 PDF 文件,避免了图片转换导致的模糊问题,同时也解决了文字无法被搜索,选中,编辑的问题。

下面,我们从pdf生成速度,生成质量,生成数量等方面对两种方案进行对比

测试的内容为生成包含文本和表格的pdf文件

1.文件生成速度对比

同样的内容,dompdf.js 生成速度更快,耗时基本上只有 html2pdf.js 的 1/2。

微信截图_20260303012415.png

2.文件体积对比

dompdf.js 生成的 pdf 文件体积更小,同样的内容页数,dompdf.js 生成的 pdf 文件体积是 html2pdf.js 的 1/5左右。

微信截图_20260303012435.png

3.清晰度对比

在放大到500%后,html2pdf.js 生成的 pdf 文字会出现明显的锯齿,而 dompdf.js 生成的 pdf 文字则完全没有压力。

html2pdf.png

html2pdf.js生成的pdf文件,放大后会有锯齿

微信截图_20260303013333.png

dompdf.js生成的矢量文件,不会出现模糊的情况

4.生成数量对比

html2pdf.js在30页左右,由于canvas高度限制,就会出现空白页,而 dompdf.js 轻松可以生成数百上千页的pdf。

微信截图_20260303014415.png

html2pdf.js生成的pdf文件,内容过多会出现空白页

微信截图_20260303015837.png

dompdf.js轻松可以生成数百上千页pdf

总结

通过上述对比可以看出,dompdf.js 在各项指标上都完胜传统的 html2pdf.js 方案。它不仅解决了 html2canvas 带来的模糊、体积大、无法选中文字等痛点,还大幅提升了生成速度和页面承载能力。

对于需要高质量、可编辑、且对性能有要求的前端 PDF 生成场景,dompdf.js 无疑是目前更优的选择。

如果你也被前端生成 PDF 的各种坑所困扰,不妨试一试这个库,希望能够帮助到你!

别忘了去 GitHub 点个 Star 支持一下作者哦!⭐⭐⭐

GitHub: github.com/lmn1919/dom…

2026年大模型怎么选?前端人实用对比

作者 牛奶
2026年3月2日 23:57

2026年大模型怎么选?前端人实用对比

这是系列第三篇。02篇我们聊完基础概念,这篇来看看怎么选对大模型和开发工具。


你有没有过这样的经历?

打开一个AI编程工具,纠结半天该选哪个模型。有人说Claude最强,有人说GPT好用,还有人说免费的DeepSeek足够用了。你花了半小时研究,最后还是随便选了一个。

结果用起来才发现:这个模型写代码总是漏这漏那,那个模型响应太慢,还有一个模型连中文都理解不好。

如果你有这样的经历,说明你和我一样,曾经被困在「选择困难」里。

这篇文章,我帮你把这件事彻底讲清楚。


原文地址

墨渊书肆/2026年大模型怎么选?前端人实用对比


先说结论

不想看长文的记住这几点:

  • 免费首选:Trae国内版(完全免费,Claude 3.5)
  • 想要最强:Cursor Pro($20/月,Claude Opus 4.6)
  • 性价比之选:Windsurf($15/月,Claude Sonnet)
  • 国产之光:智谱GLM-5(开源最强,逼近Claude)
  • 开源白嫖:OpenCode + 免费API

核心问题:模型到底差在哪?

选工具之前,先搞明白一个问题:这些模型都能写代码,到底差在哪?

根据2026年2月Coding Arena的真实投票数据(17万+开发者票选),核心差异就三点:

复杂任务处理能力

面对一个模糊的需求,顶级模型会先问清楚再做,差的模型会直接开写,然后写错。

面对跨文件重构,顶级模型能理解整个代码库的结构,差的模型只能看到当前文件。

举一个我自己的例子:

有一次我要重构一个React项目的老组件,大概3000行代码。我分别用了三个模型:

  • Claude Opus 4.5:先问我「这个组件的数据流是什么」「有没有单元测试」「目标是用Class还是Function」,然后才开始写
  • GPT-5.1:直接开始写,写到一半发现数据结构不对,又从头改了一遍
  • DeepSeek V3.2:写倒是能写,但细节处理不完善,后面我自己修了半小时

这就是差距。复杂任务面前,顶级模型不是在「写代码」,而是在「解决问题」。

思考深度

Thinking模式(推理模式)比普通模式平均强5-10%。

但Thinking模式响应慢3-8秒。

简单任务不需要Thinking,复杂任务必须开。

我的经验是:

  • 写个简单函数、开个API接口 → 普通模式就够了
  • 面对复杂需求、跨文件重构、疑难Bug → 必须开Thinking

上下文理解

有的模型看 3000 行代码就开始「失忆」,给你的代码前后矛盾。

有的模型能理解200K token,整个项目丢进去都不是问题。

前端项目越大,上下文能力越重要。特别是你要让AI帮你理解一个老项目的时候。


2026年模型排名(基于Coding Arena)

这是2026年2月的真实排名,17万开发者投票得出。数据来源:Arena.ai

第一梯队:最强王者

排名 模型 得分 适合场景
1 Claude Opus 4.6 1560 通用最强,新版无需Thinking
2 Claude Opus 4.6 Thinking 1553 架构设计、复杂重构
3 Claude Sonnet 4.6 1531 性价比最高的顶级模型

为什么强:这三兄弟是 Anthropic 家的,特点是「想清楚了再写代码」。当你面对一个复杂需求,它们会先分析问题、考虑边界情况、规划实现方案,然后才动手。

第二梯队:实用之选

排名 模型 得分 适合场景
5 GPT 5.1 High 1471 快速原型、速度优先
7 Gemini 3.1 Pro PreView 1461 多语言切换、前后端通吃
8 GLM-5 1451 开源最强,200K上下文

为什么实用:GPT 5.1 High在速度上有优势,适合快速迭代;Gemini 3.1 Pro在多语言支持上表现出色,适合全栈开发者;GLM-5虽然是国产模型,但表现已经逼近国际顶级水平,特别是在中文场景下。

第三梯队:国产新势力

排名 模型 得分 适合场景
12 kimi k2.5 thinking 1436 长文本处理、中文对话、文档分析
13 minimax m2.5 1436 多模态理解、长文本总结
17 qwen3.5 1396 阿里生态、中文优化、高性价比

为什么值得关注:国产模型正在快速追赶国际顶级水平。Kimi在长上下文和多轮对话上有优势,MiniMax在多模态领域表现出色,Qwen3.5背靠阿里云生态,性价比极高。对于国内开发者,这三个模型是很好的替代选择,特别是中文场景下体验不输国际大厂。


开发工具到底选哪个?

对于前端开发者,工具比模型更重要。因为工具已经把模型封装好了,还加了文件管理、终端操作这些功能。

1. Cursor(推荐给不差钱的)

价格:$20/月

包含模型:Claude Opus 4.6 + GPT-5.1 High + Gemini 3.1 系列

优点

  • 目前集成度最高的AI IDE
  • Tab补全、Ctrl+I提问、Ctrl+K改代码,三种模式无缝切换
  • Agent模式可以自己跑命令、改文件
  • 理解项目结构,能跨文件分析

缺点

  • 贵,$20/月
  • 国内访问不稳定

适合:预算充足,追求最强体验

我的建议:如果你只能选一个,选 Cursor。它的体验是目前最好的,特别是Agent模式,真的能帮你减少很多机械劳动。


2. Trae(国内免费首选)

价格:国内版完全免费

包含模型:Claude 3.5 Sonnet + 豆包

亮点功能

  • 国内直达:无需翻墙,直接访问
  • 中文优化:对中文Prompt理解更准确
  • 智能补全:类似Cursor的Tab补全
  • Agent模式:支持自动执行开发任务

优点

  • 免费!国内直达,不用翻墙
  • 中文体验最好
  • Claude 3.5 Sonnet足够强
  • 界面简洁,上手快

缺点

  • 相比 Cursor,集成度稍低
  • Agent 能力不如 Cursor
  • 插件生态不如 Cursor 丰富

适合:国内用户,预算0元,日常开发

我的建议:国内开发者的福音。免费且够用,夫复何求?如果你之前没用过AI编程工具,从Trae开始是最省心的选择。


3. Windsurf(性价比之选)

价格:$15/月

包含模型:Claude Sonnet系列

亮点功能

  • Flow模式:类似Cursor的Agent模式,可以自动执行多步骤任务
  • Cascade:新一代AI编程架构,任务拆解能力更强
  • 上下文保持:长时间对话中保持项目上下文

优点

  • 比Cursor便宜$5
  • 能力接近Cursor
  • Flow模式也能自动执行任务
  • 对Mac/Windows/Linux支持都很完善

缺点

  • 略逊于Cursor(主要在Agent的智能化程度)
  • 生态没Cursor成熟(插件少一些)
  • 中文优化不如Trae

适合:预算有限,但想要好体验

我的建议:如果$20觉得贵,Windsurf是完美的替代品。能力足够,价钱友好。特别是Cascade模式发布后,整体体验提升明显。


4. Google Antigravity(AI原生开发平台)

价格:免费(目前)

包含模型:Gemini 3 Pro / Flash

亮点功能

  • Agent Manager:可以同时管理多个AI Agent协同工作
  • 浏览器自动化:支持浏览器内的自动化任务执行
  • Workspace概念:支持创建多个独立的工作空间
  • Google生态集成:深度整合Google Cloud和开发工具链

优点

  • Google原生,AI Agent能力强大
  • Agent Manager可以同时管理多个AI协同工作
  • 支持浏览器内自动化任务
  • 免费!目前对开发者免费开放
  • Gemini 3在多模态理解上优势明显

缺点

  • 2025年11月才发布,还比较新
  • 生态还在建设中(插件少、功能在快速迭代)
  • 国内访问可能不稳定

适合:喜欢Google生态,想尝试最新AI编程方式的开发者

我的建议:这是Google在AI编程领域的大招。虽然还年轻,但Google的投入力度很大,未来值得关注。特别是它的「Agent Manager」概念很有意思——你可以同时让多个AI帮你干活。如果你是Google全家桶用户,强烈建议试试。


5. OpenCode(开源白嫖)

价格:完全免费(开源项目)

支持模型:75+模型,包括Claude、GPT、Gemini、DeepSeek、MiniMax M2.5

亮点功能

  • MCP扩展:支持Model Context Protocol,可以扩展各种功能
  • 灵活配置:可以自定义模型参数、API端点
  • 隐私优先:所有数据本地处理,不上传云端
  • 多模型切换:同一个对话中随时切换不同模型

优点

  • 完全免费
  • 灵活,想用啥模型用啥模型
  • 隐私优先,数据本地处理
  • 支持MCP扩展
  • 社区活跃,插件丰富
  • 支持MiniMax M2.5免费模型,国内访问稳定

缺点

  • 终端操作,有学习成本
  • 没有图形界面(纯命令行)
  • 需要自己配置API Key
  • 没有内置的代码编辑器功能

适合:开发者,有技术背景,想自己掌控

使用技巧

  • 配合VS Code的Dev Container使用效果更好
  • 推荐使用MiniMax M2.5免费模型,国内直达,无需翻墙
  • 适合需要高度定制化的专业开发者

我的建议:如果你愿意折腾,OpenCode + MiniMax M2.5是性价比最高的组合。完全免费,工具免费+模型免费,夫复何求?适合有一定技术基础、喜欢折腾的开发者。


6. Z Code(智谱官方)

价格:免费/付费

包含模型:GLM-5系列

亮点功能

  • AutoDev模式:自动完成整个开发流程(写代码→执行→测试→提交)
  • 200K超长上下文:可以一次性理解整个大型项目
  • 多模态支持:支持图片、代码、文档等多种输入形式
  • 国产化部署:支持私有化部署,适合企业用户

优点

  • 智谱官方,GLM-5体验最完整
  • 自动完成整个开发流程(写代码、执行、测试、提交)
  • 200K超长上下文
  • 中文理解能力极强
  • 国内访问稳定

缺点

  • 刚发布,生态还在建设中
  • 插件和第三方集成不如Cursor丰富
  • Agent能力还在持续优化中

适合:想体验国产最强模型、喜欢尝鲜的开发者

我的建议:GLM-5确实强,但配套工具还需要时间完善。适合想支持国产的朋友。特别是200K上下文对于大型项目非常友好,如果你需要处理大型老项目,Z Code值得一试。


预算方案推荐

预算0元:Trae + OpenCode

  • 日常开发:Trae国内版
  • 查问题:OpenCode + MiniMax M2.5免费模型
  • 尝鲜:Z Code(GLM-5)

效果:80%的日常开发够用,国产模型崛起


预算15元/月:Windsurf Pro

  • 工具:Windsurf Pro($15/月)
  • 模型:Claude Sonnet

效果:比Cursor便宜,能力足够


预算20元/月:Cursor Pro

  • 工具:Cursor Pro($20/月)
  • 模型:Claude Opus 4.6

效果:目前前端开发最强组合


想要国产最强:Z Code + GLM-5

  • 工具:Z Code(免费)
  • 模型:GLM-5(开源最强)

效果:支持国产,能力逼近Claude


我的建议

  1. 先用起来:别纠结,Trae直接下载先用
  2. 从免费开始:觉得不够再升级
  3. 按需付费:每个工具都有免费额度,先试试
  4. 组合使用:不同场景用不同工具
  5. 关注国产:GLM-5的崛起值得关注

写在最后

AI工具更新快,这篇写的是2026年2月的格局。

有一点特别想说的是:这两年国产模型的进步速度超出了所有人的预期。从2024年的「能用」,到2025年的「够用」,再到2026年的「逼近最强」——GLM-5、Kimi K2.5这些国产模型正在快速追赶。

作为前端开发者,这是最好的时代。我们有更多的选择,也有更大的空间。

下篇我们聊《Prompt怎么写才有效》——同样工具不同人用,效果差十倍。

感兴趣下篇见。

前端人为什么要学AI?

作者 牛奶
2026年3月2日 23:50

前端人为什么要学AI?

系列开篇,写给想要真正掌握未来的前端开发者。


你有没有过这样的经历?

写一个登录表单,花了半小时调样式。产品说交互要改一下,你又花了半小时。类似的功能做了无数遍,感觉自己就是个「Ctrl+C / Ctrl+V」工程师。

遇到一个复杂的正则表达式或者是算法题,对着Google搜了半小时,结果复制过来的代码自己都看不懂,最后只能硬着头皮问同事。

接手别人的代码,看着满满一屏幕的useEffectuseState,完全不知道数据是怎么流的,想改又不敢改。

如果你有过类似的经历,说明你和我一样,曾经被困在某种「技术舒适区」里。

前端会React,会写样式,会调API,但面对一些「重复性的工作」和「棘手的问题」,总是要花大量时间。

我想聊聊这件事。


原文地址

墨渊书肆/前端人为什么要学AI?


前端这件事,也被误解了很多年

一提到「前端工程师」,很多人脑海里浮现的是这样一个形象:每天跟样式打交道,调调组件,写写页面,看起来没什么技术含量。

这种理解,该过时了。

现在的Web应用越来越复杂。前端不再只是「画界面」,而是要处理复杂的交互、状态管理、性能优化、工程化建设。ReactVueNext.js......框架越来越强大,需要学的越来越多。

但问题是:

  • 前端的工作边界在扩大:以前只管页面,现在要做SSR、做SEO、做动画、做可视化......一个人要学的东西越来越多
  • 重复劳动越来越多:同样的组件改改参数就是一个新的,同样的交互换换逻辑又要重新写
  • 沟通成本越来越高:和产品经理、设计师、后端工程师来来回回确认需求,代码反而没写多少

我们变成了「高级CV工程师」——不是Copy Vector,是Copy and Paste。

这不是前端的问题,这是整个行业的痛点。


AI来了,情况不一样了

2023年开始,AI的爆发让一切变得不同。

以前我们需要自己写的代码,现在AI可以帮我们写。以前我们需要自己查的文档,现在AI可以直接读给我们听。以前我们需要自己调试的bug,现在AI可以直接帮我们定位。

但我发现一个有趣的现象:很多前端开发者对AI的态度是两个极端——

要么觉得AI没用,「生成的代码一堆bug还得我自己改」;要么觉得AI太厉害,「迟早要取代我」。

这两种观点,都不对。

AI不会取代前端,但它会重新定义「前端」这个岗位。

就像计算器没有取代数学家,但数学家必须会用计算器。AI工具不会取代前端开发者,但前端开发者必须会用AI。


AI到底能帮前端做什么?

说几个我自己的真实经历。

1. 写代码更快了

以前我要写一个日期选择器组件,从头写到尾要半小时。现在我告诉AI我的需求——「需要一个支持范围选择、禁用特定日期、暗色模式的主题适配」——它能给我一个可以直接用的版本,我只需要根据业务需求微调。

这不是「替代」,是「放大」。我原本半小时只能做一个组件,现在十分钟做出来,剩下二十分钟可以去喝杯咖啡。

2. 读代码更快了

接手别人的项目,最头疼的就是看不懂代码。现在我可以直接把代码丢给AI,让它帮我解释:「这个组件的数据流是怎么走的?为什么要用useMemo?」

它不仅能解释代码,还能帮我梳理逻辑,告诉我哪里可能有性能问题。

3. 查文档更快了

以前遇到问题,我先去Google搜,然后看Stack Overflow,最后实在不行才去翻文档。

现在我直接问AI:「Next.js 15怎么做密码重置?」它能直接给我答案,虽然不一定完全准确,但足够让我快速上手。

4. 做项目更有底气了

以前做一个带AI功能的项目,光是调研要用什么API、怎么接入、怎么管理上下文,就能劝退一半的人。

现在这些都有现成的方案。Vercel AI SDK几分钟就能搭一个聊天界面,LangChain帮我管理AI的工作流,我只需要专注于业务逻辑。


但AI不是万能的

我知道有人要问了:AI这么厉害,那我们还学什么?

这是个好问题。

我用了一年多AI辅助开发,发现它有几个明显的短板:

  • 第一,AI不懂你的业务

你告诉AI「帮我写个用户列表」,它能给你写。但你的产品里用户列表要显示会员等级、要按活跃时间排序、要支持导出Excel——这些AI不知道。

你得自己把需求翻译成AI能理解的形式。

  • 第二,AI会犯错

AI生成的代码有bug是常态,不是例外。它能帮你写70%的代码,剩下30%你得自己改、自己调。

如果你没有判断代码对不对的能力,AI帮你的可能还没有坑你的多。

  • 第三,AI不知道什么是「好」

代码能跑和代码好是两回事。AI可以写出能跑的代码,但不一定符合性能要求、安全规范、可维护性标准。

这些都需要你有一定的技术判断力。

所以,AI时代更需要学习,只是学习的内容变了。

以前我们学的是「怎么实现」,以后我们学的是「怎么整合」。

以前我们学的是「这个API怎么用」,以后我们学的是「这个需求怎么拆」。

以前我们学的是「怎么写代码」,以后我们学的是「怎么用AI写代码」。


前端学AI,有什么优势?

说了这么多,你可能会问:为什么是前端先学AI?而不是后端、不是移动端?

我的答案是:前端天然离用户最近,天然是AI落地的最佳场景。

你想做一个智能助手,第一个要做的就是界面。一个聊天窗口、一个语音按钮、一个输入框——这些是前端最擅长的。

你想做一个AI生成图片的应用,第一个要做的还是界面。用户上传图片、选择风格、预览结果——这些也是前端最擅长的。

而且前端开发者有几个天然优势:

  • 对交互敏感:我们知道什么是好的用户体验AI生成的内容需要什么样的交互来呈现
  • 对视觉敏感:我们知道怎么把AI生成的内容美化、适配不同的屏幕
  • 对技术敏感:我们天天跟API打交道,接入AI服务对我们来说轻车熟路

2026年了,如果前端还只把自己定位在「画界面」,那确实危险。但如果前端把自己定位在「用户与AI的桥梁」,那前景无限。


这个系列想带你做什么

市面上不缺AI教程。Prompt工程大模型原理LangChain实战——这种内容一搜一大把。

但我发现很多前端开发者看完这些教程,还是不知道怎么做。

因为大部分教程要么太偏理论(全是数学公式),要么太偏后端(全是Python代码),跟前端开发者的日常工作没关系。

这个系列我想带你做的事情很简单:从零开始,让AI真正成为你的开发助手。

不是demo,不是练习,而是真实的、能用到日常工作中的技能。

我会分成这几个阶段:

  • 阶段零:认知重建

    先理解AI到底能帮我们什么(就是这篇)。

  • 阶段一:Prompt工程与AI应用基础

    真正开始用AI工具。学怎么写有效的Prompt,怎么让AI帮我们写代码、查文档、修bug。

  • 阶段二:AI功能接入与网页开发

    开始做项目。把AI功能接入到自己的网页里,做出能展示的Demo。

  • 阶段三:AI原理与进阶应用

    从「会用」到「理解」。不求能自己训练模型,但求知道AI为什么有时候聪明有时候犯傻。

  • 阶段四:本地部署与生产实践

    接近实际生产。LangChain、本地模型、浏览器端运行——怎么让AI不依赖云服务也能跑。

在这个过程中,你会看到我踩过的坑,做过的错误决策,总结出的经验。我不是为了告诉你「这个技术怎么用」,而是告诉你「这个AI能力该怎么学」。


写在最后

回到开头的问题。

你是不是经常感觉写了很多代码,但真正用到的时候还是那些老东西?

这很正常。

技术本身不是目的,解决问题才是。

2026年了,AI可以帮你写代码,但不能帮你判断什么是好的代码。能做到这一点的人,永远有市场。

而这,就是我们这个系列要一起做的事情。

下一篇文章,我会讲讲《AI辅助开发的基础概念》介绍一些向量、Token、大模型的基本概念,以及前端视角怎么理解这些概念。

感兴趣的话,下一篇见。

昨天 — 2026年3月2日掘金 前端

🎉OpenTiny NEXT-SDK 重磅发布:四步把你的前端应用变成智能应用!

2026年3月2日 21:25

AI Agent 时代,人们已经不满足只是与 AI 进行问答交互,而是希望 AI 能直接帮人干活。

目前 AI 帮人干活的场景越来越丰富,最常见的就是 AI 帮人写代码、做视频、做 PPT、做设计稿。

你有没有想过 AI 能帮人操作网页?

这就是 OpenTiny NEXT-SDK 做的事情。

1 简介

OpenTiny NEXT‑SDK 是一套面向前端智能应用的开发工具包,核心是基于 MCP(Model Context Protocol) 协议,让前端应用快速接入 AI Agent,实现前端界面可被智能体直接操控的能力。

OpenTiny NEXT‑SDK 可以帮助开发者:

  • 把普通前端应用快速改造为 MCP Server,对外暴露界面操作能力

  • 让 AI Agent(WebAgent)通过标准 MCP 协议读取界面、调用功能、执行操作

  • 快速集成 AI 对话组件(如 TinyRobot),构建智能交互前端

2 项目优势

NEXT‑SDK 基于 MCP 协议实现,将 MCP 的能力扩展到了 Web 端,让 Web 应用也能被 AI 操控,以下是项目优势:

  • 扩大 MCP 工具范围:为 Agent 智能体提供更多的 MCP 工具,实现当前现有的本地/云服务 MCP 工具所不具备的能力,即操控前端应用的能力。这种能力比 RPA 方案(Browser Use / Computer Use)更快(可通过后面的演示视频感受 AI 操作的效率)、更准更经济(消耗更少 Token)

  • 完全兼容 MCP 生态:所有的前端应用都采用标准的 MCP 协议声明 MCP Server,并且基于标准的 MCP 通讯方式进行连接,比如 Streamable HTTP,意味着能完全融入现有的 MCP 生态,兼容现有乃至未来的 MCP Host 应用

  • 支持智能体交互范式:当前的前端应用主要还是人机交互,即人手动操作前端界面上的 UI 组件。引入 OpenTiny NEXT-SDK 之后,Agent 智能体可以借助 MCP 工具读取前端界面的信息、调用前端界面的功能,配合生成式 UI 实现新的智能体交互范式

  • 多样的前端智能化方案:不仅支持 Web 应用的前端智能化改造,还全面覆盖 AI 应用(对话框)的多端部署场景——无论是浏览器扩展、Web 页面集成,还是各终端内置的 AI 助手,均可直接或间接调用前端应用中的 MCP 工具

3 演示动画

我们一起来看一个演示动画(无剪辑、无加速,AI 操作页面的真实速度),直观感受下 NEXT-SDK 的能力吧!

AI创建用户.gif

接入 NEXT-SDK 的前端应用,右下角会出现一个机器人图标,点击这个图标会从侧边弹出 AI 对话框,我们可以使用自然语言与 AI 对话,让 AI 帮我们操作前端应用。

比如我们可以输入以下内容:

帮我创建以下用户,用户信息如下:
邮箱:zhangsan@sina.com
密码:Abc123456
用户名:zhangsan

这时 AI 会调用页面中定义的名为 add-user 的 MCP 工具,帮我们创建 zhangsan 这个用户。

我们提供了一个 Playground 代码演练场,你可以在线体验 NEXT-SDK 的能力。

NEXT-SDK Playground:playground.opentiny.design/next-sdk

4 快速接入

使用 OpenTiny NEXT-SDK,只需要以下四步,就可以把你的前端应用变成智能应用。

第一步:安装依赖


npm install @opentiny/next-sdk

第二步:创建 MCP Client

在 Web 应用的主入口(比如:Vue 项目的 App.vue 文件)定义 WebMcpClient。


import { onMounted, provide } from 'vue'
import { WebMcpClient, createMessageChannelPairTransport } from '@opentiny/next-sdk'

onMounted(async () => {
  // 创建通信通道
  const [serverTransport, clientTransport] = createMessageChannelPairTransport()
  provide('serverTransport', serverTransport)

  // 创建 MCP Client
  const client = new WebMcpClient()
  await client.connect(clientTransport)
  // 这个 sessionId 是 Web 应用与 WebAgent 服务建立连接后,由 WebAgent 服务生成的,用来唯一标识被操控的 Web 应用(被控端)
  const { sessionId } = await client.connect({
    agent: true,
    url: 'https://agent.opentiny.design/api/v1/webmcp-trial/mcp'
  })
})

第三步:创建 MCP Server

在 Web 应用的子页面(比如:views/page1.vue)中定义 WebMcpServer,每个页面可以定义自己的 WebMcpServer,页面切换时,MCP Client 会与当前页面的 MCP Server 建立连接,并丢弃与之前页面的连接。


import { onMounted, inject } from 'vue'
import { WebMcpServer, z } from '@opentiny/next-sdk'

onMounted(async () => {
  const serverTransport = inject('serverTransport')
  // 创建 MCP Server
  const server = new WebMcpServer({
    name: 'mcp-server-page1',
    version: '1.0.0'
  })

  // 定义 MCP 工具
  server.registerTool(
    'demo-tool',
    {
      title: '演示工具',
      description: '一个简单工具',
      inputSchema: { foo: z.string() }
    },
    async (params) => {
      console.log('params:', params)
      return { content: [{ type: 'text', text: `收到: ${params.foo}` }] }
    }
  )

  await server.connect(serverTransport)
})

完成!现在你的前端应用已经变成智能应用,可以被 AI 操控了,你可以通过各类 MCP Host 来操控智能应用。

第四步:添加 AI 遥控器

我们提供了一个开箱即用的 AI 对话框组件,支持 PC 端和移动端,就像一个遥控器,可以通过对话方式操控你的前端应用。

安装遥控器组件:


npm install @opentiny/next-remoter

在 Vue 项目中使用:


<script setup lang="ts">
import { TinyRemoter } from '@opentiny/next-remoter'
import '@opentiny/next-remoter/dist/style.css'

// 使用第二步获取的 sessionId
const sessionId = 'your-session-id'
</script>

<template>
  <tiny-remoter 
    :session-id="sessionId" 
    title="我的智能助手"
  />
</template>

遥控器会在你的应用右下角显示一个图标,悬浮后可以选择:

  • 弹出 AI 对话框:在应用侧边打开 AI 对话界面

  • 显示二维码:手机扫码后打开移动端遥控器

不管是 PC 端还是移动端,都可以通过自然语言对话的方式让 AI 帮你操作应用,极大提升工作效率!

如果你想了解更多 NEXT-SDK 的用法,请参考 NEXT-SDK 官网文档:docs.opentiny.design/next-sdk

5 立即行动

在 AI 技术快速迭代的今天,前端智能化不再是“高端需求”,而是提升产品竞争力、提升操作效率的核心能力和必选项。

OpenTiny NEXT-SDK 让前端 AI 集成,从“复杂踩坑”到“5分钟上手”,让你的应用瞬间拥有 AI 能力,领跑行业智能化创新!

立即行动,解锁前端智能化新可能:

  • 执行 npm install @opentiny/next-sdk 安装 OpenTiny NEXT-SDK,5分钟上手实操,快速体验 AI 操控效果

  • 前往 OpenTiny NEXT-SDK 官网:opentiny.design/next-sdk,查看详细的项目介绍、API 文档和进阶用法

  • 访问 OpenTiny NEXT-SDK 代码演练场:playground.opentiny.design/next-sdk,在线体验 AI 自动操作前端应用

  • 添加 OpenTiny 微信小助手:opentiny-official,加入 OpenTiny 技术交流群,获取一对一集成指导,解决实操难题,与同行交流 AI 前端集成经验

如果你有任何问题,欢迎在评论区留言交流!

ArcGIS Pro 中的 notebook 初识

作者 GIS之路
2026年3月2日 20:13

^ 关注我,带你一起学GIS ^

notebook中文翻译为笔记本,既然是笔记本,那就具有添加、修改、删除、保存等功能。ArcGIS Pro中的 notebook其实也是这意思。

区别就是ArcGIS Notebooks是一个基于JupyterLab构建的开源 web 应用程序 ,可用于创建和共享包含实时 Python 代码、可视化效果和叙事文本的文档(名为 Notebooks)。

将 ArcGIS Notebooks 集成到 ArcGIS Pro 后,可以执行分析并在地理环境中立即查看结果,与新兴数据进行交互,记录并自动化工作流,以及将其保存以供稍后使用或共享。ArcGIS Notebooks 用途包括数据清理和转换、数值模拟、统计建模、计算机学习、管理任务等。

并且ArcGIS Pro 中的所有 Python 功能均可通过 ArcGIS Notebooks 使用,其中包括核心 Python 功能、Python 标准库、ArcPyArcGIS API for Python 以及ArcGIS Pro 所随附的众多第三方库,例如 NumPy 和 pandas

ArcGIS Pro 可以使用 ArcGIS Pro 包管理器通过开源库进行扩展。

当开源Jupyter NotebooksArcGIS Pro 应用程序中本地运行时,Esri集成 Jupyter Notebook 体验也可用于ArcGIS OnlineArcGIS Enterprise门户。

1. ArcGIS Notebooks 使用

1.1. 创建一个新的笔记本。

方式一:

点击插入选项卡,在工程窗口中选择New Notebook下拉菜单,然后点击New Notebook。或者存在保存过的笔记本的话,也可以通过Add and Open Notebook打开。

方式二:

点击分析选项卡,选择Python下拉菜单,点击Python Notebook

打开notebook笔记本窗口显示如下,由标题栏、工具栏和代码区组成,主要包括保存、新建、剪切、复制、运行等工具。

1.2. 运行 Python 代码

在单元格中输入代码后,点击三角形按钮运行代码。

也可以通过按住[CTRL+ENTER]运行选定行,代码显示如下。可通过在每一行后按 Enter 键,在单个单元格内添加多行代码。 如果您习惯于在 Python 窗口或 Python 编辑器的交互式窗口中运行代码,这可能会与您的习惯不符,因为在上述两个窗口中按 Enter 键的结果是运行代码行。

2. 查看ArcGIS Notebooks

已添加到工程中的ArcGIS Notebooks将在目录窗格的 Notebooks 文件夹下列出。 使用 ArcGIS Pro 创建的 Notebook 会自动添加到您的工程中。

要将现有的笔记本添加到工程中,请右键单击Notebooks文件夹,然后选择添加笔记本,或者单击插入功能区上添加笔记本按钮旁边的下拉箭头,然后选择添加笔记本。

3. 查看代码帮助

ArcGIS Notebooks中输入代码后,可通过按下tab键打开帮助窗口查看具体方法或者属性,具有代码提示和代码补全功能。

显示列表后,还可以输入内容进行再次过滤。 从列表中选择合适的方法后,按 Enter 键即可使用该方法。

Python工具、模块、函数、类和关键字都会存储可提供有关其使用信息的文档。 通过按Shift+Tab 可以激活指针处的文档。以下是针对缓冲区工具显示的文档:

或者,也可以使用内置Python help方法访问帮助文档。以下是针对 arcpy.analysis.Clip显示的帮助文档:

4. 参考资料

  • ArcGIS Pro 中的 notebook:https://pro.arcgis.com/zh-cn/pro-app/latest/arcpy/get-started/pro-notebooks.htm
  • ArcGIS Pro 提取分析工具:https://pro.arcgis.com/zh-cn/pro-app/latest/tool-reference/analysis/clip.htm

GIS之路-开发示例数据下载,请在公众号后台回复:vector

全国信息化工程师-GIS 应用水平考试资料,请在公众号后台回复:GIS考试

GIS之路 公众号已经接入了智能 助手,可以在对话框进行提问,也可以直接搜索历史文章进行查看。

都看到这了,不要忘记点赞、收藏 + 关注

本号不定时更新有关 GIS开发 相关内容,欢迎关注 


    

GeoTools 开发合集(全)

OpenLayers 开发合集(全)

GDAL 开发合集(全)

GIS 影像数据源介绍

GeoJSON 数据源介绍

GIS 名词解释

ArcPy,一个基于 Python 的 GIS 开发库简介

GIS 开发库 Turf 介绍

GIS 开发库 GeoTools 介绍

GIS 开发库 GDAL 介绍

地图网站大全

从微信指数看当前GIS框架的趋势

Landsat 卫星数据介绍

OGC:开放地理空间联盟简介

中国地图 GeoJSON 数据集网站介绍

❌
❌