普通视图

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

深入理解 React 中的 useRef:不只是获取 DOM 元素

作者 ohyeah
2026年1月1日 20:05

在 React 函数组件的世界中,useRef 是一个常被提及但又容易被误解的 Hook。很多初学者第一次接触它时,往往只把它当作“获取 DOM 元素”的工具;而随着使用深入,又可能会疑惑:为什么不能用 useRef 替代 useState 来避免不必要的重渲染?本文将结合两个典型示例,系统梳理 useRef 的核心机制、使用场景与常见误区,帮助你真正掌握这个“默默奉献”的 Hook。


一、useRef 的基本能力:持久化引用对象

useRef 的本质是创建一个可变且持久化的引用对象。它的返回值是一个普通 JavaScript 对象,结构为 { current: initialValue }。关键在于:

  • 每次组件重新渲染时,useRef 返回的是同一个对象引用
  • 修改 .current 属性不会触发组件重新渲染
  • 它不参与 React 的响应式更新机制,因此被称为“非响应式存储”。

这与 useState 形成鲜明对比:useState 的状态变更会触发 UI 更新,而 useRef 的变更则“静默”发生。


二、场景一:获取 DOM 元素并操作

最常见的 useRef 用法是绑定到 JSX 元素上,从而在组件逻辑中直接操作 DOM。

import { useRef, useEffect, useState } from 'react';

export default function App() {
  const [count, setCount] = useState(0);
  const inputRef = useRef(null);

  useEffect(() => {
    // 组件挂载后,inputRef.current 指向真实的 <input> 元素
    inputRef.current.focus();
  }, []);

  return (
    <>
      <input ref={inputRef} />
      {count}
      <button onClick={() => setCount(count + 1)}>count ++</button>
    </>
  );
}

在这个例子中:

  • 初始渲染时,inputRef.currentnull,因为 DOM 尚未生成;
  • React 在完成 DOM 挂载后,会自动将对应的 DOM 节点赋值给 ref.current
  • useEffect(依赖项为空数组)在首次挂载后执行,此时 inputRef.current 已是有效的 <input> 元素,调用 .focus() 实现自动聚焦。

值得注意的是:即使 inputRef.currentnull 变为 DOM 节点,组件也不会重新渲染。这正是 useRef 的设计初衷——提供一种不干扰 React 渲染流程的方式来访问或存储数据。


三、场景二:存储可变值以避免状态重置

除了操作 DOM,useRef 还非常适合用于在多次渲染之间持久化存储可变值,尤其是在处理副作用(如定时器)时。

考虑以下错误写法:

// ❌ 错误:使用普通变量存储定时器 ID
let intervalId = null;

function start() {
  intervalId = setInterval(() => {
    console.log('tick~~~');
  }, 1000);
}

function stop() {
  clearInterval(intervalId); // 可能为 null!
}

问题在于:每当 count 状态更新,整个函数组件会重新执行,let intervalId = null 会被再次初始化,导致之前保存的定时器 ID 丢失。结果是:

  • 多次点击“开始”会创建多个定时器;
  • “停止”按钮无法清除旧的定时器;
  • 造成内存泄漏逻辑混乱

正确的做法是使用 useRef

import { useEffect, useRef, useState } from 'react';

export default function App() {
  const intervalId = useRef(null);
  const [count, setCount] = useState(0);

  function start() {
    intervalId.current = setInterval(() => {
      console.log('tick~~~');
    }, 1000);
  }

  function stop() {
    clearInterval(intervalId.current);
  }

  return (
    <>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
      {count}
      <button onClick={() => setCount(count + 1)}>count ++</button>
    </>
  );
}

这里的关键在于:

  • useRef(null) 在组件整个生命周期内始终返回同一个对象
  • 即使 count 更新导致组件重新渲染,intervalId.current 依然保留着上次设置的定时器 ID;
  • 因此 clearInterval 能正确清除目标定时器,避免资源泄露。

四、useRef 与 useState 的核心区别

虽然 useRefuseState 都能“存值”,但它们的设计目标截然不同:

特性 useState useRef
是否触发重渲染 ✅ 是 ❌ 否
值变更是否被 React 跟踪 ✅ 是(通过调度更新) ❌ 否
适用场景 驱动 UI 变化的状态 存储不需触发 UI 更新的可变数据
初始值 每次调用 useState(initial) 仅在首次生效 useRef(initial) 的初始值也仅在首次生效,但后续 .current 可任意修改

常见误区:能否用 useRef 替代 useState?

不能。

假设你试图用 useRef 存储计数器值以“避免重渲染”:

const countRef = useRef(0);
// ...
<button onClick={() => countRef.current++}>+1</button>
<p>{countRef.current}</p>

你会发现:点击按钮后,页面上的数字不会更新!因为 React 并不知道 countRef.current 发生了变化,自然不会重新执行渲染逻辑。

结论

  • 如果你需要同时存储值并更新 UI,必须使用 useState
  • 如果你只需要在不触发重渲染的前提下保存中间状态或引用(如定时器 ID、前一次的 props、滚动位置等),才应使用 useRef

五、useRef 的典型应用场景总结

  1. 访问 DOM 节点
    如聚焦输入框、测量元素尺寸、触发动画等。

  2. 持久化存储可变值

    • 定时器/延时器 ID(setInterval / setTimeout
    • WebSocket 实例
    • 第三方库的实例(如地图、图表对象)
    • 上一次的 props 或 state(用于对比)
  3. 跨渲染保持状态而不触发更新
    例如记录组件是否已挂载(在异步回调中判断是否还能安全 setState)。


结语

useRef 虽然名字里有 “ref”,但它远不止是“获取 DOM 的工具”。它是一个轻量级、非响应式的持久化容器,在需要“记住某些东西但又不想打扰 React 渲染流程”时大显身手。

正确使用 useRef,能让你的组件更高效、更健壮;而误用它(如试图替代状态管理),则会导致 UI 不更新或逻辑错乱。理解其“非响应式”和“引用持久化”的两大特性,是掌握这一 Hook 的关键。

在实际开发中,当你遇到以下情况时,不妨想想 useRef

  • “我需要在组件里保存一个值,但它变了不需要刷新页面。”
  • “我想在挂载后操作某个 DOM 元素。”
  • “我的定时器怎么关不掉了?是不是 ID 丢了?”

答案,往往就在 useRef 之中。

Vue Router 深度解析:从基础概念到高级应用实践

2026年1月1日 18:15

引言

在现代Web应用开发中,单页应用(SPA)已经成为主流开发模式。Vue.js作为当前最流行的前端框架之一,其官方路由库Vue Router在构建复杂SPA应用中扮演着至关重要的角色。本文将通过分析一个完整的Vue Router项目实例,深入探讨Vue Router的核心概念、使用方法和最佳实践,帮助开发者全面掌握这一强大的路由管理工具。

一、Vue Router基础概念解析

1.1 SPA应用的本质

单页Web应用(Single Page Web Application, SPA)的核心特点是整个应用只有一个完整的HTML页面。与传统的多页应用不同,SPA在用户与应用程序交互时,不会重新加载整个页面,而是通过JavaScript动态更新页面的部分内容。这种模式带来了更流畅的用户体验,减少了页面切换时的白屏时间,使应用更接近原生应用的体验。

从提供的代码中可以看到,App.vue作为应用的根组件,通过<router-view>标签动态渲染不同的路由组件,这正是SPA架构的典型实现。

1.2 路由的基本概念

在Vue Router中,路由本质上是路径(path)与组件(component)之间的映射关系。这种映射关系使得当用户访问特定URL时,能够展示对应的Vue组件。

javascript

复制下载

// 路由配置示例
const router = new VueRouter({
  routes: [
    {
      path: '/about',
      component: UserAbout
    },
    {
      path: '/home',
      component: UserHome
    }
  ]
})

二、Vue Router的核心配置与使用

2.1 路由器的创建与配置

index.js文件中,我们可以看到完整的路由器配置示例。路由器通过VueRouter构造函数创建,接收一个配置对象,其中routes数组定义了所有的路由规则。

javascript

复制下载

import VueRouter from 'vue-router'
import UserAbout from '../pages/UserAbout.vue'
import UserHome from '../pages/UserHome.vue'

const router = new VueRouter({
  routes: [
    {
      name: 'guanyu',
      path: '/about',
      component: UserAbout,
      meta: { title: '关于' }
    }
  ]
})

2.2 路由组件与一般组件的区别

Vue Router的一个重要实践是将路由组件和一般组件分离存储:

  • 路由组件通常存放在pagesviews文件夹中
  • 一般组件通常存放在components文件夹中

这种分离有助于代码的组织和维护,使项目结构更加清晰。从代码中可以看到,UserAboutUserHome等路由组件都存放在pages目录下,而UserBanner这样的展示组件则存放在components目录下。

2.3 多级路由(嵌套路由)的实现

Vue Router支持嵌套路由,允许在父路由组件中嵌套子路由组件。这在构建复杂布局时非常有用。

javascript

复制下载

{
  name: 'zhuye',
  path: '/home',
  component: UserHome,
  children: [
    {
      name: 'xinwen',
      path: 'news',  // 注意:此处不加/,表示相对路径
      component: UserNews
    },
    {
      name: 'xiaoxi',
      path: 'message',
      component: UserMessage,
      children: [
        {
          name: 'xiangqing',
          path: 'detail',
          component: MessageDetail
        }
      ]
    }
  ]
}

UserHome.vue组件中,通过<router-view>标签来渲染子路由组件,实现了嵌套路由的展示。

三、路由参数传递的多种方式

3.1 Query参数传递

Query参数是Vue Router中最常用的参数传递方式之一。它通过URL的查询字符串传递参数,适合传递可选参数。

vue

复制下载

<!-- UserMessage.vue中的query参数传递 -->
<router-link :to="{
  path: '/home/message/detail',
  query: {
    id: message.id,
    title: message.title
  }
}">
  {{message.title}}
</router-link>

在目标组件MessageDetail.vue中,可以通过$route.query获取这些参数:

vue

复制下载

<template>
  <ul>
    <li>id: {{$route.query.id}}</li>
    <li>title: {{$route.query.title}}</li>
  </ul>
</template>

3.2 Params参数传递

Params参数通过URL路径的一部分传递,适合传递必选参数。使用params参数时需要注意路由配置:

javascript

复制下载

{
  path: 'detail/:id/:title',  // 定义params参数
  component: MessageDetail
}

传递params参数时,必须使用name配置而非path配置:

vue

复制下载

<router-link :to="{
  name: 'xiangqing',
  params: {
    id: message.id,
    title: message.title
  }
}">
  {{message.title}}
</router-link>

3.3 Props配置简化参数接收

Vue Router提供了props配置,可以将路由参数作为组件的props传递,使组件更加独立和可复用。

javascript

复制下载

{
  name: 'xiangqing',
  path: 'detail',
  component: Detail,
  
  // 第一种写法:props值为对象
  props: { a: 900 }
  
  // 第二种写法:props值为布尔值
  props: true  // 将params参数作为props传递
  
  // 第三种写法:props值为函数
  props(route) {
    return {
      id: route.query.id,
      title: route.query.title
    }
  }
}

四、编程式路由导航

除了使用<router-link>进行声明式导航外,Vue Router还提供了编程式导航API,使路由跳转更加灵活。

4.1 基本导航方法

javascript

复制下载

// UserMessage.vue中的编程式导航示例
methods: {
  pushShow(m) {
    this.$router.push({
      path: '/home/message/detail',
      query: {
        id: m.id,
        title: m.title
      }
    })
  },
  replaceShow(m) {
    this.$router.replace({
      path: '/home/message/detail',
      query: {
        id: m.id,
        title: m.title
      }
    })    
  }
}

4.2 历史记录管理

Vue Router提供了多种历史记录管理方法:

javascript

复制下载

// UserBanner.vue中的历史记录控制
methods: {
  forward() {
    this.$router.forward();
  },
  back() {
    this.$router.back();
  },
  go() {
    this.$router.go(-2);  // 后退两步
  }
}

五、高级路由功能

5.1 缓存路由组件

在某些场景下,我们需要保持组件的状态,避免组件在切换时被销毁。Vue Router通过<keep-alive>组件实现路由组件的缓存。

vue

复制下载

<!-- UserHome.vue中的缓存配置 -->
<keep-alive include="UserNews">
  <router-view></router-view>
</keep-alive>

include属性指定需要缓存的组件名称(注意是组件name,不是路由name)。如果需要缓存多个组件,可以使用数组:

vue

复制下载

<keep-alive :include="['UserNews', 'UserMessage']">
  <router-view></router-view>
</keep-alive>

5.2 组件生命周期扩展

当使用<keep-alive>缓存组件时,Vue组件会获得两个新的生命周期钩子:

javascript

复制下载

// UserNews.vue中的生命周期示例
activated() {
  console.log('UserNews组件被激活了');
  // 启动定时器
  this.timer = setInterval(() => {
    this.opacity -= 0.01;
    if(this.opacity <= 0) {
      this.opacity = 1;
    }
  }, 16)
},
deactivated() {
  console.log('UserNews组件被停用了');
  // 清除定时器
  clearInterval(this.timer);
}

这两个钩子只在被缓存的组件中生效,分别在组件激活和失活时触发,非常适合处理如定时器、事件监听等需要在组件不显示时清理的资源。

六、路由守卫系统

路由守卫是Vue Router中用于权限控制和路由拦截的强大功能。

6.1 全局守卫

全局守卫作用于所有路由,包括全局前置守卫和全局后置守卫。

javascript

复制下载

// 全局前置守卫
router.beforeEach((to, from, next) => {
  // 权限检查示例
  if(to.meta.isAuth) {
    alert('需要授权才能查看');
    // 可以在这里进行登录检查等操作
  } else {
    next();  // 放行
  }
});

// 全局后置守卫
router.afterEach((to, from) => {
  // 修改页面标题
  document.title = to.meta.title || '默认标题';
});

6.2 独享守卫

独享守卫只作用于特定路由,在路由配置中直接定义:

javascript

复制下载

{
  path: '/about',
  component: UserAbout,
  beforeEnter(to, from, next) {
    // 进入/about路由前的逻辑
    next();
  }
}

6.3 组件内守卫

组件内守卫定义在组件内部,提供了更细粒度的控制:

javascript

复制下载

// UserAbout.vue中的组件内守卫
export default {
  name: 'UserAbout',
  
  // 进入守卫
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件的对应路由被验证前调用
    // 不能获取组件实例 `this`
    next();
  },
  
  // 离开守卫
  beforeRouteLeave(to, from, next) {
    // 在导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
    next();
  }
}

七、路由模式选择

Vue Router支持两种路由模式:hash模式和history模式。

7.1 Hash模式

Hash模式使用URL的hash部分(#后面的内容)来模拟完整的URL,而不触发页面重新加载。这是Vue Router的默认模式。

特点:

  • 兼容性好,支持所有浏览器
  • 不需要服务器端特殊配置
  • URL中带有#号,不够美观

7.2 History模式

History模式利用HTML5 History API实现,提供了更清洁的URL。

javascript

复制下载

const router = new VueRouter({
  mode: 'history',  // 启用history模式
  routes: [...]
})

特点:

  • URL更美观,没有#号
  • 需要服务器端配置支持,避免刷新时出现404错误
  • 兼容性相对较差(现代浏览器都支持)

八、项目架构与最佳实践

8.1 项目结构组织

基于提供的代码,我们可以总结出良好的Vue Router项目结构:

text

复制下载

src/
├── components/     # 一般组件
│   └── UserBanner.vue
├── pages/         # 路由组件
│   ├── UserAbout.vue
│   ├── UserHome.vue
│   ├── UserNews.vue
│   ├── UserMessage.vue
│   └── MessageDetail.vue
├── router/        # 路由配置
│   └── index.js
└── App.vue       # 根组件

8.2 路由配置管理

对于大型项目,建议将路由配置拆分到多个文件中:

javascript

复制下载

// router/modules/home.js
export default {
  path: '/home',
  component: () => import('@/pages/UserHome.vue'),
  children: [...]
}

// router/index.js
import homeRoutes from './modules/home'
import aboutRoutes from './modules/about'

const routes = [
  homeRoutes,
  aboutRoutes
]

8.3 路由懒加载

使用Webpack的动态导入语法实现路由懒加载,可以显著提升应用性能:

javascript

复制下载

{
  path: '/about',
  component: () => import(/* webpackChunkName: "about" */ '../pages/UserAbout.vue')
}

九、常见问题与解决方案

9.1 路由重复点击警告

当重复点击当前激活的路由链接时,Vue Router会抛出警告。可以通过以下方式解决:

javascript

复制下载

// 方法1:在push时捕获异常
this.$router.push('/path').catch(err => {
  if (err.name !== 'NavigationDuplicated') {
    throw err
  }
})

// 方法2:重写VueRouter原型方法
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
  return originalPush.call(this, location).catch(err => err)
}

9.2 页面滚动行为控制

Vue Router允许自定义页面滚动行为:

javascript

复制下载

const router = new VueRouter({
  routes: [...],
  scrollBehavior(to, from, savedPosition) {
    // 返回滚动位置
    if (savedPosition) {
      return savedPosition
    } else {
      return { x: 0, y: 0 }
    }
  }
})

十、总结

Vue Router作为Vue.js生态中不可或缺的一部分,为构建复杂的单页应用提供了完整的路由解决方案。从基础的路由配置到高级的守卫系统,从简单的参数传递到复杂的嵌套路由,Vue Router都提供了简洁而强大的API。

通过本文对示例代码的深入分析,我们可以总结出使用Vue Router的最佳实践:

  1. 合理组织项目结构:分离路由组件和一般组件,使代码更易维护
  2. 合理使用路由参数:根据场景选择query或params参数传递方式
  3. 善用路由守卫:实现灵活的权限控制和路由拦截
  4. 优化组件生命周期:合理使用缓存和对应的生命周期钩子
  5. 考虑路由模式:根据项目需求选择合适的路由模式
  6. 实现代码分割:使用路由懒加载优化应用性能

随着Vue 3的普及,Vue Router 4也带来了更多新特性和改进,但核心概念和设计思想与Vue Router 3保持一致。掌握本文介绍的核心概念和实践技巧,将为开发者构建高效、可维护的Vue.js应用奠定坚实基础。

cloudflare的worker中的Environment环境变量和不同环境配置

作者 1024小神
2026年1月1日 18:11

大家好,我是1024小神,想进 技术群 / 私活群 / 股票群 或 交朋友都可以私信我,如果你觉得本文有用,一键三连 (点赞、评论、关注),就是对我最大的支持~

在cloudflare中配置不同的环境变量和环境是开发中肯定会遇到的,比如密钥不能明文存储,比如开发环境和测试环境隔离,这里的配置和在vite中配置环境变量还是不一样的,所以这里记录一下。官方文档:developers.cloudflare.com/workers/wra…

环境变量

环境变量的文档:developers.cloudflare.com/workers/wra…

或者在wrangler.jsonc同级目录配置.env文件:注意.env文件不应该被git记录

API_HOST="value"
API_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"

就是在wrangler.jsonc中定义变量名称,然后在代码中获取:

export default {
  async fetch(request, env, ctx) {
    return new Response(`API host: ${env.API_HOST}`);
  },
};

这里有更详细的用法说明:developers.cloudflare.com/workers/con…

当然wrangler.jsonc定义的是配置会被git同步到仓库中,肯定是不安全的,所以这里配置的一定是不重要的或测试环境的变量,在后台worker中可以配置生产环境的变量:

不同的环境

为不同的环境配置不同的环境变量也是必须的,这里有两种方式,一个是在Wrangler.jsonc中配置,另外一个就是通过配置文件.env.test、.env.prod等实现,就和在前端中配置一样简单。我这里推荐使用配置文件的方式,因为这种方式可以避免环境变量泄漏风险。

配置.env.test文件:

使用命令启动:

wrangler dev --env test

就可以看到加载的环境变量:

或者写一个接口来查询环境变量信息:

得到的结果:

如果你有好的想法或需求,都可以私信我,我这里有很多程序员朋友喜欢用代码来创造丰富多彩的计算机世界

终端环境:zsh、oh-my-zsh与 7 个效率插件

作者 ____xl
2026年1月1日 17:40

终端环境:zsh、oh-my-zsh、提示主题与 7 个效率插件

本文主要介绍如何构建一个高效的终端环境,包括 zsh 与 bash 的对比、zsh 的安装、oh-my-zsh 框架的使用与主题配置,以及 7 个实用插件(含 5 个内置插件 + 2 个社区插件),提升命令行效率。


1. 为什么使用 zsh?

zsh(Z Shell)是一个功能强大的 shell,相较 bash 有以下优势::

  • 更强的自动补全:不仅补全命令,还能补全参数、选项和文件名,同时可显示简短帮助提示。
  • 更好的脚本与插件支持:拥有活跃社区和丰富插件生态,可大幅增强 shell 功能。
  • 主题与提示符高度可定制:自定义命令行外观和显示内容(如 Git 分支、环境信息等)。
  • 智能交互体验:支持拼写纠正与近似完成等功能,提高使用便捷性。
  • 增强文件匹配及配置灵活性:支持扩展通配符和历史共享等高级特性。

2. 安装 zsh

在不同操作系统下安装 zsh 的方式各不相同,常见命令示例:

# Debian / Ubuntu
apt install zsh

# CentOS
yum install -y zsh

# Arch
pacman -S zsh

# Fedora
dnf install zsh

macOS 自 2019 起默认使用 zsh,无需手动安装。若需要安装或更新,可使用 brew install zsh。安装后,用 chsh -s /bin/zsh 将 zsh 设置为默认 shell。


3. oh-my-zsh 框架

oh-my-zsh 是一个用于管理 zsh 配置的轻量框架,内置大量主题与插件。安装命令如下:

sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

安装完成后,将自动启用基础主题与插件。可通过编辑 ~/.zshrc 来更改主题和插件配置。


4. 主题配置

oh-my-zsh 内置了多个提示符主题,可在 ~/.zshrc 中更改:

ZSH_THEME="agnoster"

也可将 ZSH_THEME 设置为 random,随机选取主题。


5. 内置插件(5 个)

插件启用方式

~/.zshrc 中指定:

plugins=(git web-search jsontools z vi-mode)

插件介绍

  1. git – 提供常用 Git 命令别名,提高操作效率。
  2. web-search – 在终端直接打开浏览器并执行搜索。
  3. jsontools – 提供 JSON 格式化等基本处理工具。
  4. z – 基于历史访问目录的快速跳转工具。
  5. vi-mode – 允许使用 vi 键盘模式编辑命令行。

6. 社区插件(2 个)

这两款插件需要从 GitHub 下载并配置到 oh-my-zsh:

git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-syntax-highlighting ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting

并将其加入插件列表:

plugins=(... zsh-syntax-highlighting zsh-autosuggestions)

插件详情

  • zsh-syntax-highlighting:提供命令实时语法高亮,语法错误时以不同颜色提示。
  • zsh-autosuggestions:根据历史命令和补全提示提供智能建议,可通过自定义快捷键(如 Tab)来接受建议。

7. 总结

一般以上插件基本满足99%的场景使用,如果其它推荐欢迎补充!

dify案例分享-免费体验Dify + Qwen-Image-2512 文生图图生图全流程

作者 wwwzhouhui
2026年1月1日 17:32

1.前言

在AI图像生成领域快速迭代的今天,如何用低成本、低门槛的方式体验最新的文生图、图生图技术,成为了小伙伴们关注的焦点。传统的AI绘画工具要么需要复杂的本地部署、要么需要高昂的API调用费用,普通用户想要"玩转"AI绘画往往望而却步。

好家伙!阿里通义千问团队这2天又放大招了!继8月发布Qwen-Image基础模型后,12月又重磅推出了Qwen-Image-2512文生图模型,同时11月发布的Qwen-Image-Edit-2511图生图模型也正式上线魔搭社区。这两款模型在AI Arena超过1万局的用户盲测中,开源模型表现最优,甚至与多款闭源模型对比中依然展现出显著竞争力!

image-20260101154038359

之前给大家做过一个基于Qwen-Image文生图 和图生图的dify插件,今天上午也升级了。另外也使用最新的Qwen-Image-2512验证测试了一下。今天我们就在Dify平台手把手教大家部署这个AI绘画工作流,体验和感受一下这两款最新模型的强大能力。话不多说,我们开始吧!

2.模型介绍

在正式开始工作流制作之前,我们先来了解一下这次更新的两款重磅模型。

Qwen-Image-2512(文生图模型)

Qwen-Image-2512是阿里巴巴通义千问团队于2025年12月发布的最新文生图模型,相较于8月发布的Qwen-Image基础模型,本次聚焦于三大核心能力的飞跃式提升:

✨ 三大核心升级

升级项 能力描述 效果说明
更真实的人物质感 精准刻画皮肤纹理、发丝走向、表情神态 告别塑料脸、模糊五官,还能理解"微微前倾"等语义细节
更细腻的自然纹理 水流、苔藓、动物毛发等细节刻画 金毛犬的绒毛、盘羊的粗硬皮毛,达到"显微镜级别"的细腻度
更复杂的文字渲染 精准排版时间轴、技术图表、多格漫画 图文混合不再是痛点,中文渲染能力业界领先

更真实的人物质感

image-20260101171907960

更细腻的自然纹理

9184a942-4d86-4ab7-a882-3f79ec58a51d

更复杂的文字渲染

jimeng-2026-01-01-4091-这是一张现代风格的科技感幻灯片,整体采用深蓝色渐变背景。标题是 “Dify 发展...

🏆 性能表现

在AI Arena超过1万局的用户盲测中,Qwen-Image-2512在开源模型中表现最优,并在与多款闭源模型的对比中依然展现出显著竞争力。

1767259065580

Qwen-Image-Edit-2511(图生图模型)

Qwen-Image-Edit-2511是2025年11月发布的增强版图像编辑模型,是Qwen-Image-Edit-2509的升级版本,专注于高级图像编辑任务。

✨ 核心特点

特性 说明
一致性显著提升 减轻图像漂移,角色身份保持,风格一致性更强
多人一致性增强 两张不同人物图像高保真融合,实现"隔空合照"
LoRA原生集成 照明增强、新视角生成开箱即用,无需额外加载权重
工业设计能力 批量产品设计、材质替换、高保真渲染
几何推理增强 自动生成辅助构造线,适用于建筑设计、工程图纸

image-20260101153547398

image-20260101153621027

🎯 应用场景

  • 创意摄影: 隔空合照、人像创意编辑、多人合成
  • 电商产品: 产品场景变换、材质替换、批量生成
  • 工业设计: 零部件材质调整、设计方案对比
  • 内容创作: 社交媒体、营销物料、风格化处理

image-20260101153658437

版本对比

对比项 旧版Qwen-Image 新版Qwen-Image-2512 提升幅度
人物质感 存在AI感 接近真实摄影 ⬆️⬆️ 显著提升
自然纹理 细节一般 显微镜级别 ⬆️⬆️ 显著提升
文字渲染 中文较弱 复杂排版支持 ⬆️⬆️ 显著提升
图生图 不支持 Qwen-Image-Edit-2511 🆕 全新功能

之前给大家介绍过dify插件开发,其中使用就是阿里Qwen-Image模型。当时实现的是文本生成模型。前段时间我也把这个插件上传到dify插件市场了。

img

最近有小伙伴给我反馈这个插件不支持图片修改功能,同时官方也发布了最新的Qwen-Image-2512和Qwen-Image-Edit-2511模型。于是我更新了这个插件,目前已经支持最新的文生图和图生图模型了。工作流效果如下:

img

那么这个工作流是如何制作的呢?下面给大家简单介绍一下。

3.工作流制作

插件安装

制作这个工具流之前我们先去dify插件市场查找这个插件。搜索关键字"Text2image" 新版本插件我已经提交了,注意最新版本是0.0.4

如果没有的可以在文本找一下离线插件。

img

搜到到这个插件后安装即可。

img

安装或者更新这插件后,我们可以在魔搭API进行相关授权。

魔搭API配置

去魔搭社区官方网站找到你的API

img

把这个值复制到刚才的插件api key输入区域

img

这样我们就完成模型授权。

img

接下来我们给大家介绍一下工作流详细步骤。

开始节点

这个开始节点有2个部分组成:type类型 和 picture 图片

type类型是一个下拉选项,主要是提供用户的文生图、图生图选择项

img

picture 图片是由单个文件图片构成

img

以上我们就完成了开始节点的配置。

条件分支

条件分支这里我们可以实现文生图和图生图的判断。我们可以设置如下信息

img

文生图(Qwen-Image-2512)

这个地方就是我们可以从添加节点-选择我们上面安装好的插件。

img

我们选中文生图插件。

提示词部分我们直接获取sys.query

模型这里我们选择最新的Qwen-Image-2512(相比旧版Qwen-Image,新版在人物质感、自然纹理、文字渲染方面有显著提升)

img

图生图(Qwen-Image-Edit-2511)

图生图和上面文生图的操作类似。从添加节点 - 工具选择 图生图

img

它的配置多了一个图像URL选择,模型选择最新的Qwen-Image-Edit-2511(支持一致性保持、多人融合、LoRA原生集成等高级功能)

img

直接回复

这个直接回复比较简单,就是把文生图和图生视频的信息返回

img

以上我们就配置了最简单的基于Qwen-Image-2512和Qwen-Image-Edit-2511插件的文生图、图生图功能了。

有的小伙伴说这个文生图的提示词太简单了,能不能给我扩写成一个专业的基于Qwen-Image的提示词呢?当然这个也是可以的。

提示词生成

我这里有一份Qwen-Image提示词指南

核心要点:
抓重点:主体 + 背景 + 细节,不要跑题
补特征:人要写清姿态表情,物要写清材质颜色
写文字:用引号标明,还要写清位置和字体
定风格:纪实/国风/童趣,风格统一更稳定
理空间:左上右下,前后层级要讲明
正向写:别说"不要",直接说你要什么
去赘余:画面里没的东西,就别写

万能模版骨架:
[主体] + [环境/背景] + [构图/镜头] + [风格/质感] + [光线/色调] + [空间/关系] + [需生成文字]
示例:一只黑色猫咪,坐在木质桌上,中景拍摄,写实摄影风格,午后柔光,猫在左下角,"Good Day"文字写在右上角,手写体、浅绿色

小技巧:
把否定词改成正向表达:
"不要复杂背景""纯色背景"
"不要太暗""整体偏明亮"
"不要拥挤""留白充足"

请基于以上内容编写一个编写提示词,使用LangGPT提示词(prompt)语法编写一个Qwen-Image文生图提示词专家。

我们把上面的提示词发给AI让它给我们生成出来

img

img

AI很快就帮我生成好提示词了。

img

LLM大语言模型

我们把上面生成的提示词在上面制作好的工作流增加一个LLM大语言优化后的节点,这样我们简单的提示词就通过Qwen-Image文生图提示词专家润色了生成更加专业的提示词了。

模型这里我们选择魔搭社区提供的免费的qwen3-Coder-30B-A3B-Instruct模型

img

系统提示词

# Role: Qwen-Image文生图提示词专家

## Profile
- Author: 周辉
- Version: 1.0
- Language: 中文
- Description: 专业的Qwen-Image文生图提示词编写专家,擅长根据用户需求生成高质量、结构化的图像生成提示词

## Skills
1. 熟练掌握Qwen-Image模型的提示词规则和特点
2. 能够将用户模糊描述转化为精确的结构化提示词
3. 擅长运用万能模版骨架进行提示词构建
4. 精通正向表达技巧,避免否定词使用
5. 熟悉各种艺术风格和拍摄技法的专业术语

## Rules
1. 严格遵循"抓重点、补特征、写文字、定风格、理空间、正向写、去赘余"七大核心要点
2. 必须使用万能模版骨架:[主体] + [环境/背景] + [构图/镜头] + [风格/质感] + [光线/色调] + [空间/关系] + [需生成文字]
3. 所有否定表达必须转换为正向表达
4. 人物描述必须包含姿态和表情
5. 物体描述必须包含材质和颜色
6. 文字内容用引号标明,并说明位置和字体
7. 空间关系要明确(左上右下、前后层级)
8. 避免描述画面中不存在的元素

## Workflow
1. **需求分析**:理解用户的图像需求,识别关键元素
2. **要素提取**:从用户描述中提取主体、背景、风格等核心要素
3. **结构构建**:按照万能模版骨架组织提示词结构
4. **正向优化**:将所有否定表达转换为正向描述
5. **细节补充**:为人物补充姿态表情,为物体补充材质颜色
6. **质量检查**:确保提示词符合七大核心要点

## OutputFormat
【提示词】:[按万能模版骨架生成的完整提示词]

【解析说明】:
- 主体:[说明主体描述要点]
- 环境背景:[说明背景设定]
- 构图镜头:[说明拍摄角度和构图]
- 风格质感:[说明艺术风格]
- 光线色调:[说明光影效果]
- 空间关系:[说明元素位置布局]
- 文字要求:[如有文字需求,说明内容和样式]

## Example
用户需求:我想要一张可爱的小女孩在花园里的照片

【提示词】:一位5岁小女孩,扎着双马尾,灿烂笑容,穿粉色连衣裙,站在五彩花园中,中景竖构图,童趣插画风格,温暖金色阳光,女孩居中偏右,花朵环绕四周,"Happy Garden"文字位于左上角,手写体、浅蓝色

【解析说明】:
- 主体:5岁小女孩,补充了发型、表情、服装等特征
- 环境背景:五彩花园,明确了背景元素
- 构图镜头:中景竖构图,适合人物拍摄
- 风格质感:童趣插画风格,符合主题调性
- 光线色调:温暖金色阳光,营造愉悦氛围
- 空间关系:女孩居中偏右,花朵环绕,层次清晰
- 文字要求:指定了文字内容、位置、字体和颜色

## Initialization
你好!我是Qwen-Image文生图提示词专家。我将根据Qwen-Image的特点和最佳实践,为您生成高质量的文生图提示词。

请告诉我您想要生成什么样的图像,我会运用专业的结构化方法,为您量身定制精准的提示词。无论是人物、风景、静物还是抽象艺术,我都能帮您转化为Qwen-Image能够完美理解的描述语言。

用户提示词

请根据用户输入的{{#sys.query#}}扩展这个文生图提示词

img

添加后的LLM大语言模型后,text-to-image这里输入提示词需要修改成从llm大语言模型输入

img

以上我们就通过LLM大语言模型扩展了文生图提示词。

4.验证及测试

文生图测试

76d642e1-c092-4f51-b4f0-1e15b4669d82

image-20260101162541732

图生图测试

e8801994-56b3-49b4-ad9b-e4841f770b14

【提示词】:猴子头上带个紧箍咒

image-20260101163537514

Qwen-Image-2512 新特性体验

Qwen-Image-2512在人物质感方面有了显著提升,我们来体验一下:

人物质感测试提示词示例

一位中国女性大学生,性别女,年龄约20岁左右,超短发发型略带柔和文艺感,发丝自然垂落遮住部分脸颊,整体风格偏向假小子(tomboy)气质。她肤色冷白,五官清秀,表情略显羞涩又带着一丝拽劲,嘴角微微歪起,流露出痞帅又青春的神态。身穿一字领露肩短袖上衣,露出一侧肩膀,身材匀称。画面为近景自拍构图,人物占据主体位置,背景清晰可见宿舍环境。

image-20260101164018155

自然纹理测试提示词示例

一只花猫的超写实特写肖像,置于柔和自然日光下的户外场景中;毛发细节极为精细 —— 根根分明,橘白黑三色的斑纹自然交错,色泽从暖橘色到纯净白色再到深邃黑色过渡得丝滑流畅,微光在毛尖轻盈跳跃,微风拂过带来轻微蓬松感;底层绒毛柔软浓密,外层护毛修长分明,层次清晰可见;双眼清澈湿润、富有情感,像透亮的琉璃珠子,鼻头微润并带有细腻的高光反光。

image-20260101165330096

新版模型在这些场景下的表现确实令人惊艳,皮肤纹理、发丝走向、动物毛发都能精准刻画到"显微镜级别"。

体验地址

工作流地址:dify.duckcloud.fun/chat/rk31bv…

备用地址:http://14.103.204.132/chat/rk31bvsH0gWasqDW

插件下载

离线安装包: qwen_text2image_0.0.4.difypkg

通过网盘分享的文件:qwen_text2image_0.0.4.difypkg 链接: pan.baidu.com/s/1EK5mJxJA… 提取码: segu

5.总结

今天主要带大家了解并实现了基于Dify工作流构建Qwen-Image-2512文生图、Qwen-Image-Edit-2511图生图功能的完整流程,该流程以阿里巴巴通义千问团队最新发布的"Qwen-Image-2512 + Qwen-Image-Edit-2511"双模型为核心,结合Dify平台灵活的工作流节点配置(如条件分支、插件调用、LLM提示词优化等),形成了一套覆盖文本生成图像、图像编辑修改的全场景AI绘画解决方案。

通过这套实践方案,小伙伴们能够低成本体验Qwen-Image最新版本的强大生成能力——借助魔搭社区提供的免费模型接口和Dify平台的便捷配置(包括插件安装、API授权、工作流搭建),无需复杂的本地部署和高昂的API费用,就能快速实现文生图的精准生成和图生图的风格统一修改(如本次演示的"螃蟹打架+乌龟裁判"案例)。无论是人物质感刻画、自然纹理渲染,还是复杂文字排版、多人场景融合,都能通过Qwen-Image-2512和Qwen-Image-Edit-2511配合LLM提示词优化完成,极大降低了AI图像创作的使用门槛。在实际应用中,该工作流不仅支持Qwen-Image-2512在人物皮肤纹理、发丝走向、动物毛发等细节的"显微镜级别"刻画,还支持Qwen-Image-Edit-2511的一致性保持、多人融合、LoRA原生集成等高级功能,适配性远优于传统的单一文生图方案;特别是通过LLM大语言模型对提示词进行专业化扩写,有效解决了普通用户调用AI绘画时提示词不够专业、生成效果不理想的难题。

同时,方案具备良好的扩展性——小伙伴们可以基于此扩展更多实用场景,如自媒体的创意素材生成、电商产品的场景变换与材质替换、工业设计的批量渲染、建筑设计的效果图展示等,进一步发挥Qwen-Image系列模型在内容创作、电商运营、工业设计、教育培训等领域的应用价值。感兴趣的小伙伴可以按照文中提供的步骤进行实践,根据实际业务需求调整提示词和工作流配置。今天的分享就到这里结束了,我们下一篇文章见。

从零开始调用大模型:使用 OpenAI SDK 实现歌词生成,手把手实战指南

作者 栀秋666
2026年1月1日 17:23

引言

在 AIGC 浪潮席卷全球的今天,大语言模型(LLM) 已不再是科研实验室里的“黑科技”,而是每一个开发者都能轻松调用的强大工具。

而作为行业标杆的 OpenAI,通过其简洁高效的 API 和完善的 SDK 支持,让我们只需几行代码就能让 AI 写诗、作词、编程甚至写周报!

本文将以一个真实场景为例——让 GPT 为汪峰写一首献给“森林北”的情歌,带你从环境搭建到文本生成,完整走通一次大模型调用流程。

全程无坑、代码可运行,新手友好,老鸟也能收获细节技巧,建议收藏+点赞,防止走丢!


🌱 一、开发准备:5分钟初始化项目

我们使用 Node.js + npm 搭建后端服务环境,轻量高效,适合快速验证 AI 能力。

1. 初始化项目

npm init -y

这条命令会自动生成 package.json 文件,记录你的项目依赖和配置信息。

2. 安装 OpenAI SDK

npm install openai

OpenAI 官方提供的 SDK 封装了所有 API 接口,省去手动处理 HTTP 请求的繁琐工作,一行引入,即刻调用

⚠️ 注意:SDK 需要 Node.js 版本 >= 16,请确保本地环境满足要求。


🔑 二、配置客户端:拿到通往 AI 世界的钥匙

接下来是关键一步:创建 OpenAI 客户端实例

import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: 'sk-RUP7SvQy4trgMCDsbBXxjgpNSR235Kqa7tjIh8jv1NBlMnzI', // 替换为你自己的 Key
  baseURL: 'https://api.302.ai/v1' // 可选:代理地址,解决网络访问问题
});

关键参数说明:

参数 说明
apiKey 访问权限凭证,相当于“密码”,务必保密!
baseURL API 地址,默认为 https://api.openai.com/v1,此处使用第三方代理(如无法直连 OpenAI 可用)

💡 小贴士

  • Key 可在 OpenAI 官网 获取。
  • 若你在国内,推荐使用稳定中转服务(如 302.ai、FastGPT 等),避免请求超时。

🎤 三、调用模型:让 AI 成为林夕级别的作词人

我们的目标很明确:

“以林夕风格,为汪峰写一首关于‘森林北’的爱情歌曲,100 字左右。”

使用 completions.create() 接口

这是 OpenAI 最经典的文本生成接口,适用于单次输入、输出任务,比如写文案、写歌词、补全文本等。

const response = await openai.completions.create({
  model: 'gpt-3.5-turbo-instruct',
  prompt: `
    假如你是林夕这样的爱情歌曲作词大家,
    请你写一首100字,为汪峰,写一首他爱上森林北的歌曲。
    森林北是一位美丽,勇敢,会骑马的女孩儿。
  `,
  max_tokens: 800,
  temperature: 1
});

参数详解:

参数 作用 推荐值
model 指定模型版本 gpt-3.5-turbo-instruct 性价比之王
prompt 提示词,决定生成内容方向 描述越细,结果越准 ✅
max_tokens 控制最大输出长度 一般设为 512~1024
temperature 创意随机性控制 0.7~1.0 平衡创意与稳定性

🎯 Prompt 工程技巧

  • 明确角色:“假如你是林夕”
  • 给出对象:“汪峰爱上了森林北”
  • 设定特征:“会骑马、勇敢、自由的灵魂”
  • 限制格式:“100字以内,押韵优先”

✅ 优质 Prompt = 高质量输出!


📤 四、获取结果:把 AI 的灵感打印出来

API 返回的是 JSON 格式数据,我们需要从中提取生成的文本。

const song = response.choices[0].text.trim();
console.log('🎵 歌词是:\n' + song);

示例输出(模拟):

image.png 是不是已经有汪峰那味儿了?🎸


🛡️ 五、健壮性增强:加入错误处理机制

实际开发中不能忽略异常情况。网络波动、Key 失效、请求超时都可能导致程序崩溃。

建议用 try/catch 包裹调用逻辑:

try {
  const response = await openai.completions.create({ /* ... */ });
  const song = response.choices[0].text.trim();
  console.log('✅ 成功生成歌词:\n' + song);
} catch (error) {
  console.error('❌ 调用失败:', error.message);
  if (error.status === 401) {
    console.log('👉 检查 API Key 是否正确');
  }
}

常见错误码:

  • 401: Key 错误
  • 429: 请求频率过高
  • 500: 模型服务器异常

🔄 六、进阶思考:Completion vs Chat 接口怎么选?

你可能会问:现在主流都是 chat.completions,为什么还用 completion

对比表格:

特性 completions chat.completions
适用场景 单轮生成任务 多轮对话系统
输入格式 纯文本 prompt 消息数组(role-based)
上下文记忆 ❌ 不支持 ✅ 支持历史对话
成本 较低 略高(尤其长上下文)
推荐用途 写作、摘要、填空 客服机器人、聊天应用

📌 结论

  • 如果只是一次性生成内容(如写歌词、写邮件),completion 更简单直接;
  • 如果要做智能对话系统,必须上 chat 接口。

🚀 七、拓展玩法:你可以这样玩得更嗨

学会了基础调用,下一步就是创造价值!以下是一些延展思路:

1. 批量生成歌词片段

结合数据库或 CSV,批量为不同人物生成专属情歌。

2. 构建 Web 页面

用 Express 或 Next.js 搭个网页,让用户填写“歌手+恋人名字+关键词”,实时生成歌词。

3. 微调模型(Fine-tuning)

收集林夕风格歌词进行微调,打造专属“林夕Bot”。

4. 结合语音合成

用 TTS(Text-to-Speech)把歌词念出来,做成 AI 演唱 Demo!


💬 八、结语:掌握核心能力,才能驾驭 AI 浪潮

通过这篇文章,你已经掌握了:

✅ 如何初始化 Node.js 项目
✅ 如何安装并配置 OpenAI SDK
✅ 如何编写高质量 Prompt
✅ 如何调用 completion 接口生成文本
✅ 如何处理异常、优化用户体验

更重要的是,你理解了:

大模型不是魔法,而是工具;真正的魔法,在于你怎么使用它。

无论你是前端、后端、全栈还是产品经理,只要学会调用大模型,就能为自己赋能十倍效率。


面试官 : “ 说一下 localhost 和127.0.0.1 的区别 ? ”

作者 千寻girling
2026年1月1日 17:01

localhost 是主机名(域名) ,属于应用层概念;

127.0.0.1 是IPv4 回环地址,属于网络层概念。

两者都用于访问本机服务,但 localhost 必须通过解析才能映射到具体 IP(默认是 127.0.0.1 或 IPv6 的 ::1),而 127.0.0.1 是直接的网络层标识,无需解析。


一、本质定义与协议层次

概念 localhost 127.0.0.1
本质 互联网标准规定的特殊主机名(RFC 6761 定义) IPv4 协议规定的回环地址(RFC 5735 定义)
协议层次 应用层(DNS 协议解析范畴) 网络层(IP 协议寻址范畴)
归属 属于域名系统(DNS) 属于 IP 地址体系
默认映射 IPv4: 127.0.0.1;IPv6: ::1 仅 IPv4 回环网段(127.0.0.0/8)的第一个地址

关键补充

  1. 127.0.0.0/8 网段:不只是 127.0.0.1,整个 127.x.x.x 网段(共 16777216 个地址)都属于回环地址,访问任何一个都会指向本机。
  2. localhost 的特殊性:它是一个保留主机名,不能被注册为公共域名,且操作系统会优先通过 hosts 文件解析,而非公共 DNS 服务器。

二、解析流程的根本差异

这是两者最核心的区别 ——是否需要解析,以及解析的顺序

1. localhost 的解析流程(应用层 → 网络层)

当你在浏览器输入 http://localhost:3000 时,操作系统会执行以下步骤:

  1. 检查本地 hosts 文件

    • Windows 路径:C:\Windows\System32\drivers\etc\hosts
    • Linux/macOS 路径:/etc/hosts
    • 如果 hosts 文件中有如下映射:127.0.0.1 localhost 或 ::1 localhost,则直接使用对应的 IP。
  2. 若 hosts 文件无映射,查询本地 DNS 缓存

    • 操作系统会检查之前是否解析过 localhost,若有缓存则直接使用。
  3. 若缓存无结果,查询本地 DNS 服务器

    • 但由于 localhost 是保留主机名,公共 DNS 服务器通常也会返回 127.0.0.1 或 ::1
  4. 解析完成后,转换为 IP 地址进行网络请求

    • 此时才进入网络层,使用解析后的 IP 连接本机服务。

2. 127.0.0.1 的访问流程(直接进入网络层)

当你输入 http://127.0.0.1:3000 时,跳过所有解析步骤

  1. 操作系统直接识别这是一个 IPv4 回环地址。
  2. 直接将网络请求发送到本机的网络接口(回环接口,lo 接口)。
  3. 目标服务监听 127.0.0.1 或 0.0.0.0 时,即可响应请求。

三、功能与使用上的具体差异

1. 协议支持差异

  • localhost:支持 IPv4 和 IPv6 双协议

    • 若你的系统开启了 IPv6,localhost 可能优先解析为 ::1(IPv6 回环地址)。
    • 例如:在 Node.js 中,server.listen(3000, 'localhost') 会同时监听 IPv4 的 127.0.0.1:3000 和 IPv6 的 ::1:3000
  • 127.0.0.1仅支持 IPv4

    • 无论系统是否开启 IPv6,使用 127.0.0.1 都只会走 IPv4 协议。
    • 例如:server.listen(3000, '127.0.0.1') 仅监听 IPv4 地址。

2. 性能差异

  • 127.0.0.1 略快:因为跳过了 DNS 解析流程(即使是本地 hosts 文件解析,也需要一次文件读取和匹配)。
  • 差异极小:在开发环境中,这种性能差异几乎可以忽略不计,除非是高频次的请求(如每秒上万次)。

3. 服务监听的差异

服务端程序的监听地址,会影响是否能被 localhost 或 127.0.0.1 访问:

监听地址 能否被 localhost 访问 能否被 127.0.0.1 访问 能否被局域网其他设备访问
localhost ✅(IPv4 解析时)
127.0.0.1 ✅(解析为 127.0.0.1 时)
0.0.0.0 ✅(通过本机局域网 IP)
::1(IPv6) ✅(解析为 ::1 时)

4. 自定义映射的差异

  • localhost 可以被自定义映射

    • 你可以修改 hosts 文件,将 localhost 映射到任意 IP,例如:

      192.168.1.100   localhost
      
    • 此时访问 localhost 会指向局域网的 192.168.1.100,而不是本机。

  • 127.0.0.1 无法被自定义

    • 它是 IPv4 协议规定的回环地址,无论如何修改配置,访问 127.0.0.1 都只会指向本机。

5. 兼容性差异

  • 老旧系统 / 服务:某些非常古老的程序(如早期的 DOS 程序、嵌入式设备程序)可能不识别 localhost 主机名,但一定能识别 127.0.0.1
  • IPv6 专属服务:某些服务仅监听 IPv6 的 ::1,此时只能通过 localhost 访问(解析为 ::1),而 127.0.0.1 无法访问。

四、实际开发中的选择建议

  1. 优先使用 localhost

    • 理由:兼容性更好,支持双协议,符合开发习惯,且无需关心 IPv4/IPv6 配置。
    • 场景:本地开发、测试环境、前端代理配置(如 Vite、Webpack 的 devServer.host: 'localhost')。
  2. 使用 127.0.0.1 的场景

    • 强制使用 IPv4:当服务仅监听 IPv4 地址,或系统 IPv6 配置有问题时。
    • 避免自定义映射:当你怀疑 hosts 文件被修改,localhost 被映射到非本机地址时。
    • 某些工具的特殊要求:部分 CLI 工具或服务(如某些数据库客户端)默认只识别 127.0.0.1
  3. 特殊场景:0.0.0.0

    • 这不是回环地址,而是通配地址,表示监听本机所有网络接口(包括回环接口、局域网接口、公网接口)。
    • 场景:需要让局域网其他设备访问本机服务时(如手机测试前端页面)。

五、验证两者差异的小实验

实验 1:修改 hosts 文件,观察 localhost 映射

  1. 打开 /etc/hosts(Linux/macOS)或 C:\Windows\System32\drivers\etc\hosts(Windows)。
  2. 添加一行:192.168.1.1 localhost
  3. 执行 ping localhost,会发现 ping 的是 192.168.1.1,而非 127.0.0.1
  4. 执行 ping 127.0.0.1,仍然 ping 本机。
  5. 恢复 hosts 文件默认配置:127.0.0.1 localhost 和 ::1 localhost

实验 2:查看服务监听的地址

  1. 在 Node.js 中运行以下代码:

    const http = require('http');
    const server = http.createServer((req, res) => {
      res.end('Hello World!');
    });
    // 监听 localhost
    server.listen(3000, 'localhost', () => {
      console.log('Server running on localhost:3000');
    });
    
  2. 执行 netstat -tulpn | grep 3000(Linux/macOS)或 netstat -ano | findstr 3000(Windows)。

  3. 会发现服务同时监听 127.0.0.1:3000 和 ::1:3000(IPv4 + IPv6)。

  4. 若将监听地址改为 127.0.0.1,则仅监听 127.0.0.1:3000


六、总结:核心区别一览表

对比维度 localhost 127.0.0.1
本质 主机名(域名) IPv4 回环地址
协议层次 应用层(DNS) 网络层(IP)
解析需求 必须解析(hosts → DNS) 无需解析
协议支持 IPv4 + IPv6 仅 IPv4
自定义映射 可通过 hosts 文件修改 不可修改,固定指向本机
服务监听 可同时监听 IPv4/IPv6 仅监听 IPv4
兼容性 现代系统支持,老旧系统可能不支持 所有支持 IPv4 的系统都支持
性能 略慢(解析开销) 略快(无解析开销)

我是千寻, 这期内容到这里就结束了,我们有缘再会😂😂😂 !!!

详解把老旧的vue2+vue-cli+node-sass项目升级为vite

作者 mikan
2026年1月1日 16:21

🎯 实在是太慢了,影响开发效率,

📄 既然做了那就彻底点,原来的yarn 也换为了 pnpm , node-sass 换为了 dart-sass, vue-cli 换为 vite 7+

首先要做好思想准备,你的耐心是有限的,但是面对你的是无穷无尽的报错和各种奇怪的原因跑不起来的深渊,不过我觉得报错才是我们的朋友,因为白屏才是最可怕的,什么信息也没有,要自己去找

下面就开始了,我们想一想,如果升级打包工具的话,哪些写法要变呢,聪明的你应该已经想到了,原来依赖vue-cli(webpack)环境的部分肯定是用不了了的,所以我们要找到这些替代方案,比如垫片和适配器和改为新的api方法.还有就是原来解析.vue文件的loader没了,现在要换人了那就是 vite-plugin-vue2 ,还有其他的插件和loader,根据你的项目而定,都需要被替换.

大部分插件都可以在这里面找到 vite插件列表

1 更换环境

首先vite和 vite-plugin-vue2 装上,有两个版本,2.7以上和非2.7以上版本,我这边项目很老,而且还有些原来外包魔改后的库依赖2.5,我就不升级了,老实用vue2.5x的版本

Vue2 plugin for Vite

Vite plugin for Vue 2.7

之后就是需要告诉vite如何打包我们的项目,我们只需要写vite.config.js就好了

  • 其中 ClosePlugin 是解决因为打包的时候会挂在built in 那个地方不正确退出
  • vueDevTools 在vue2中用不了,看了下issue,只能用浏览器插件了,所以只能 codeInspectorPlugin 将就下了
  • css预处理器,一般情况下都不需要配置,只需要安装sass等依赖就好了,但是我这边不一样,原来外包的人用了很多的骚操作
  • 至于babel和core-js,我直接移除,都这么多年了你还用不支持这些语法的浏览器是不是该升级一下了😊😊😊
  • 其他的配置视情况而定,在插件列表中找到对应的插件就好

下面是我的配置,给大家用来参考

import { defineConfig, loadEnv } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'
import { codeInspectorPlugin } from 'code-inspector-plugin'
import { resolve } from 'path'
import ClosePlugin from './vite-plugin-close.js'

// 使用 defineConfig 定义 Vite 配置
export default defineConfig(({ mode, command }) => {
  const env = loadEnv(mode, process.cwd())
  console.log(env)

  return {
    base: './',
    server: {
      port: 8090,
      host: true,
      open: true,
      proxy: {
        '/api-test': {
          target: 'http://172.20.203.30:18000',
          changeOrigin: true,
          rewrite: (p) => {
            const s = p.replace('/api-test', '')
            // console.log(`proxy to test =>  ${p}`);
            return s
          }
        }
      }
    },

    // 配置插件
    plugins: [
      ClosePlugin(),
      codeInspectorPlugin({
        bundler: 'vite'
      }),

      //  oops vue2 不支持 😭😭😭
      // vueDevTools({
      //   launchEditor: env.VITE_LAUNCH_EDITOR ?? 'code'
      // }),
    ],
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: (content, filename) => {
            return getAdditionalData(filename) + content
          },
          // 或者指定禁用特定警告
          silenceDeprecations: [
            'legacy-js-api',      // 旧版 JS API
            'global',             // 全局变量
            'import',             // @import 语法
            'color',              // 旧的颜色函数
            'division',           // 除法运算
            'mixin',              // 混合器警告
            'selector'           // 选择器警告
          ]
        }
      }
    },

    // 配置模块解析规则
    resolve: {
      // 配置路径别名
      alias: {
        '@': resolve('src'),
        'element-ui-theme': resolve('node_modules/element-ui/packages/theme-chalk/src')
      },
      // https://cn.vitejs.dev/config/#resolve-extensions
      extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
    }
  }
})

function getAdditionalData(filePath) {
  // 根据文件路径决定是否注入
  if (filePath.includes('xr-theme.scss')) {
    return '' // 不向 xr-theme.scss 中的文件注入
  }

  return `
    @import "@/styles/xr-theme.scss";
    @import "element-ui/packages/theme-chalk/src/common/var.scss";
  `
}
css 相关
  • 首先如果你换了dart-sass 的话,会有很多的报错 比如 /deep/ 需要用 ::v-deep 来替换

  • 原来scss中还用了这种特殊的语法,在js中使用css变量

// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
  colorPrimary: $--color-primary; //0052CC
  colorBlack: $--color-black; //  172B4D
  colorSuccess: $--color-success; //  36B37E
  colorWarning: $--color-warning; //  FFAB00
  colorDanger: $--color-danger; //  FF5630
  colorN40: $--color-n40; //  DFE1E6
}

在vite中暂时没有找到很好的支持,所以建立了一个js文件用于存放这些变量

export const XRTheme = {
  colorPrimary: '#0052CC',
  colorBlack: '#172B4D',
  colorSuccess: '#36B37E',
  colorWarning: '#FFAB00',
  colorDanger: '#FF5630',
  colorN40: '#DFE1E6',

  //   ......
}

之后用regex全局替换,要把全部的import xrTheme from '@/styles/xr-theme.scss' 这种替换为 import { XRTheme as xrTheme } from '@/common/cssTheme.js'.

大概就是这个意思

"import xrTheme from '@/styles/xr-theme.scss'".replace(/import +(xrTheme) +from +'@\/styles\/xr-theme.scss'/,"import { XRTheme as $1} from '@/common/cssTheme.js'")

原来人还有一个骚操作,全局注入导入

在vue-cli中的配置如下

const oneOfsMap = config.module.rule('scss').oneOfs.store
oneOfsMap.forEach((item) => {
  item
    .use('sass-resources-loader')
    .loader('sass-resources-loader')
    .options({
      resources: [
        resolve('src/styles/xr-theme.scss'),
        resolve(
          'node_modules/element-ui/packages/theme-chalk/src/common/var.scss'
        )
      ]
    })
    .end()
})

vite中scss有支持,我写为函数形式是因为他们要注入 @import "@/styles/xr-theme.scss"; 但是向 xr-theme.scss 文件注入会无限递归,所以需要限制,但是不知道为什么vue-cli中能自动解决这个问题😒

css: {
  preprocessorOptions: {
    scss: {
      additionalData: (content, filename) => {
        return getAdditionalData(filename) + content
      },
    }
  }
}


function getAdditionalData(filePath) {
  // 根据文件路径决定是否注入
  if (filePath.includes('xr-theme.scss')) {
    return '' // 不向 xr-theme.scss 中的文件注入
  }

  return `
    @import "@/styles/xr-theme.scss";
    @import "element-ui/packages/theme-chalk/src/common/var.scss";
  `
}

当然还会有些警告,比如scss某些语法已经过时了,但是感觉暂时也没有很好的方法来换他们,因为感觉不是一行regex能搞定的,最好有语法分析,至于官方的迁移工具是不支持.vue文件的.所以暂时没时间搞了

这一通操作下来改了180+个文件

js和环境相关

原来导入了一些函数,但是不存在,cli中不会报错,但是vite中会找不到,需要对这些前人留下的坑全部给填掉

process.env.VUE_APP_CONTACT_URL 这种变量需要被替换为 import.meta.env.VITE_CONTACT_URL 记得VUE开头要换位VITE开头,不然找不到

process.env.VUE_APP_([^ ]+) 替换为 import.meta.env.VITE_($1)

require api替换 require('@/assets/callCenter/ring.png') 替换为 new URL('@/assets/img/head.png', import.meta.url).href

regex 如下 "require('@/assets/callCenter/ring.png')".replace(/require\('([^']+)'\)/,"new URL('$1', import.meta.url).href")

其中store 和 router 使用了动态导入,但是require.context是webpack的语法,看vite官方文档,有一个api比较像,注意eager要为true,不然返回的是一个Map<string,Promise<Module>>的类型

importAll(require.context('./modules', false, /.js$/)) 替换为 importAll(import.meta.glob('./modules/*.js', { eager: true }))

importAll中是modules.keys().forEach()写法,需要替换为Object.keys(modules),而且key可能和原来预想的不同,我就是因为这个原因直接白屏了,因为后面模块名不对导致加载逻辑全错,下面贴出代码对比

 const syncRouters = []
 const asyncRouterMap = []
 
-function importAll(r) {
+function importAll(modules) {
   let manageIndex = -1
-  r.keys().forEach(key => {
-    const fileName = key.slice(2, -3)
-    const itemRouter = r(key).default
+
+  Object.keys(modules).forEach(moduleKey => {
+    const fileName = moduleKey.slice(2, -3)
+    const itemRouter = modules[moduleKey].default
+    console.log(fileName,moduleKey,itemRouter)
+
     if (fileName.includes('sync')) {
       syncRouters.push(...itemRouter)
     } else {
       asyncRouterMap.unshift(asyncRouterMap.splice(oaIndex, 1)[0])
     }
   }
 }
 
-importAll(require.context('./modules', false, /\.js$/))
+importAll(import.meta.glob('./modules/*.js', { eager: true }))
 
 export {
   syncRouters,

原来还在vue代码中用过path.resolve 这种nodejs中的api,直接写一个垫片resolve之后导入,全局替换导入模块,当然你也可以写vite插件替换或者模拟虚拟模块解析之后映射,至于实现我是找ai写的就不贴了

 import { mapGetters } from 'vuex'
 import { getNavMenus } from './components/utils'
-import path from 'path'
+import { path } from '@/common/path'
 
 export default {
   name: 'CrmLayout',
           auth = this.$auth(item.permissions.join('.'))
         }
         if (!item.hidden && auth) {
           sideMenus.push({
             ...item,
             path: path.resolve(mainPath, item.path)

这一波又是60+个文件的修改

第三方依赖兼容

外包使用的vue2-org-tree 的库中找不到某个文件,其实是路径解析的问题,我们需要明确后缀,注意:vite会查看pkg.json,优先使用module

这个操作需要改源码,直接用pnpm patch功能就好,非常方便

//   pkg.json
{
    "main": "dist/index.js",
    "module": "src/main.js",
}

//   index.js
- import Vue2OrgTree from './components/org-tree'
+ import Vue2OrgTree from './components/org-tree.vue'

项目中外包使用了自己魔改的el-ui库,并且基于魔改库重写了插件,比如 el-bigdata-table 中的 render.js 用到了jsx语法,感觉没必要为了一个依赖引入jsx,所以建了一个子工程,写一套打包配置来打包为h函数版本,最后拷贝到项目,聪明的你可能要问为什么要自己写打包配置,因为他的包中只有源码,没有上传打包配置😂,下面是jsx版本

export default function render(h, oldRender) {
  return (
    <div
      style={[{height: `${this.table.virtualBodyHeight}px`}]}
      class={['el-table__virtual-wrapper', {'el-table--fixed__virtual-wrapper': this.fixed}]}
      v-mousewheel={this.table.handleFixedMousewheel}
    >
      <div style={[{transform: `translateY(${this.table.innerTop}px)`}]}>
      {
        oldRender.call(this, h)
      }
      </div>
    </div>
  );
}
打包部分

打包还有点小插曲,就是卡built in xxx.xxxs这里,看国外stack overflow中和一些文章中说要写个插件,其实就是在结束的钩子中调用 process.exit(0) 系统调用

export default function ClosePlugin() {
  return {
    name: 'ClosePlugin', // required, will show up in warnings and errors

    // use this to catch errors when building
    buildEnd(error) {
      if (error) {
        console.error('Error bundling')
        console.error(error)
        process.exit(1)
      } else {
        console.log('Build ended')
      }
    },

    // use this to catch the end of a build without errors
    closeBundle(id) {
      console.log('Bundle closed')
      process.exit(0)
    }
  }
}

模板文件修改,主要是加入 <script type="module" src="/src/main.js"></script>,这样vite才知道入口

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <link rel="icon" href="favicon.ico">
    <title>CRM</title>
  </head>
  <body>
    <noscript>
    </noscript>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

记得发布的vite的base参数也要调整,nginx也是

可能还有遗漏,但是大概就是这些了,感谢观看😊😊😊!

C# 上位机开发中的动态绑定与虚拟化

作者 七宝三叔
2026年1月1日 16:16

一、动态绑定 (Dynamic Binding)

1. 数据绑定基础

csharp

// Model类
public class DeviceData : INotifyPropertyChanged
{
    private string _deviceName;
    private double _temperature;
    private DateTime _timestamp;
    
    public string DeviceName
    {
        get => _deviceName;
        set
        {
            _deviceName = value;
            OnPropertyChanged();
        }
    }
    
    public double Temperature
    {
        get => _temperature;
        set
        {
            _temperature = value;
            OnPropertyChanged();
            OnPropertyChanged(nameof(TemperatureFormatted));
        }
    }
    
    public DateTime Timestamp
    {
        get => _timestamp;
        set
        {
            _timestamp = value;
            OnPropertyChanged();
        }
    }
    
    // 计算属性
    public string TemperatureFormatted => $"{_temperature:F1}°C";
    
    // INotifyPropertyChanged 实现
    public event PropertyChangedEventHandler PropertyChanged;
    
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

2. 动态绑定集合

csharp

// ViewModel
public class DeviceViewModel : INotifyPropertyChanged
{
    private ObservableCollection<DeviceData> _devices;
    private DeviceData _selectedDevice;
    
    public ObservableCollection<DeviceData> Devices
    {
        get => _devices;
        set
        {
            _devices = value;
            OnPropertyChanged();
        }
    }
    
    public DeviceData SelectedDevice
    {
        get => _selectedDevice;
        set
        {
            _selectedDevice = value;
            OnPropertyChanged();
        }
    }
    
    // 动态添加设备
    public void AddDevice(string name)
    {
        var device = new DeviceData
        {
            DeviceName = name,
            Temperature = 25.0,
            Timestamp = DateTime.Now
        };
        
        Devices.Add(device);
    }
    
    // 动态更新数据
    public void UpdateDeviceTemperature(string deviceName, double temperature)
    {
        var device = Devices.FirstOrDefault(d => d.DeviceName == deviceName);
        if (device != null)
        {
            device.Temperature = temperature;
            device.Timestamp = DateTime.Now;
        }
    }
}

3. XAML绑定示例

xml

<!-- MainWindow.xaml -->
<Window x:Class="MonitorApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    
    <Grid>
        <!-- DataGrid动态绑定 -->
        <DataGrid ItemsSource="{Binding Devices}"
                  SelectedItem="{Binding SelectedDevice}"
                  AutoGenerateColumns="False"
                  VirtualizingPanel.IsVirtualizing="True">
            <DataGrid.Columns>
                <DataGridTextColumn Header="设备名称" 
                                    Binding="{Binding DeviceName}"/>
                <DataGridTextColumn Header="温度" 
                                    Binding="{Binding TemperatureFormatted}"/>
                <DataGridTextColumn Header="更新时间" 
                                    Binding="{Binding Timestamp, StringFormat=HH:mm:ss}"/>
            </DataGrid.Columns>
        </DataGrid>
        
        <!-- 实时数据显示 -->
        <ItemsControl ItemsSource="{Binding Devices}"
                      VirtualizingPanel.IsVirtualizing="True">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Border Margin="5" Padding="10" Background="LightGray">
                        <StackPanel>
                            <TextBlock Text="{Binding DeviceName}" 
                                       FontWeight="Bold"/>
                            <TextBlock Text="{Binding TemperatureFormatted}" 
                                       Foreground="Red"/>
                        </StackPanel>
                    </Border>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
    </Grid>
</Window>

二、虚拟化 (Virtualization)

1. UI虚拟化实现

csharp

// 自定义虚拟化数据源
public class VirtualizedDataSource<T> : IList, INotifyCollectionChanged
{
    private readonly List<T> _data;
    private readonly int _pageSize;
    private Dictionary<int, T> _cache;
    
    public VirtualizedDataSource(IEnumerable<T> data, int pageSize = 100)
    {
        _data = data.ToList();
        _pageSize = pageSize;
        _cache = new Dictionary<int, T>();
    }
    
    // 虚拟化访问
    public object this[int index]
    {
        get
        {
            if (!_cache.ContainsKey(index))
            {
                // 加载数据页
                LoadPage(index / _pageSize);
            }
            return _cache[index];
        }
        set => throw new NotImplementedException();
    }
    
    private void LoadPage(int pageNumber)
    {
        int start = pageNumber * _pageSize;
        int end = Math.Min(start + _pageSize, _data.Count);
        
        for (int i = start; i < end; i++)
        {
            _cache[i] = _data[i];
        }
    }
    
    public int Count => _data.Count;
    
    // 实现其他必要接口...
}

// 使用示例
public class LargeDataViewModel
{
    public VirtualizedDataSource<DeviceData> VirtualizedDevices { get; }
    
    public LargeDataViewModel()
    {
        // 生成大量数据
        var data = Enumerable.Range(1, 100000)
            .Select(i => new DeviceData
            {
                DeviceName = $"设备_{i}",
                Temperature = 20 + i % 30,
                Timestamp = DateTime.Now.AddSeconds(-i)
            });
        
        VirtualizedDevices = new VirtualizedDataSource<DeviceData>(data);
    }
}

2. 高级虚拟化配置

xml

<!-- 虚拟化ListView -->
<ListView ItemsSource="{Binding VirtualizedDevices}"
          VirtualizingPanel.IsVirtualizing="True"
          VirtualizingPanel.VirtualizationMode="Recycling"
          VirtualizingPanel.ScrollUnit="Pixel"
          VirtualizingPanel.CacheLength="100,100"
          ScrollViewer.CanContentScroll="True"
          ScrollViewer.IsDeferredScrollingEnabled="True">
    
    <ListView.View>
        <GridView>
            <GridViewColumn Header="ID" 
                            DisplayMemberBinding="{Binding DeviceName}"/>
            <GridViewColumn Header="温度"
                            DisplayMemberBinding="{Binding TemperatureFormatted}"/>
        </GridView>
    </ListView.View>
    
    <ListView.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel VirtualizingPanel.ScrollUnit="Pixel"/>
        </ItemsPanelTemplate>
    </ListView.ItemsPanel>
</ListView>

<!-- 虚拟化TreeView -->
<TreeView VirtualizingStackPanel.IsVirtualizing="True"
          VirtualizingStackPanel.VirtualizationMode="Recycling">
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Children}">
            <TextBlock Text="{Binding Name}"/>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

3. 异步虚拟化加载

csharp

public class AsyncVirtualizedCollection<T> : ObservableCollection<T>, IList, INotifyCollectionChanged
{
    private readonly Func<int, Task<IEnumerable<T>>> _dataLoader;
    private readonly int _pageSize;
    private readonly Dictionary<int, List<T>> _loadedPages;
    private int _totalCount;
    
    public AsyncVirtualizedCollection(
        Func<int, int, Task<IEnumerable<T>>> dataLoader, 
        int totalCount, 
        int pageSize = 50)
    {
        _dataLoader = (page) => dataLoader(page * pageSize, pageSize);
        _totalCount = totalCount;
        _pageSize = pageSize;
        _loadedPages = new Dictionary<int, List<T>>();
    }
    
    // 重写索引器实现虚拟化
    public new T this[int index]
    {
        get
        {
            int page = index / _pageSize;
            int pageIndex = index % _pageSize;
            
            if (!_loadedPages.ContainsKey(page))
            {
                // 异步加载页面
                LoadPageAsync(page).Wait();
            }
            
            return _loadedPages[page][pageIndex];
        }
        set => base[index] = value;
    }
    
    private async Task LoadPageAsync(int page)
    {
        var data = await _dataLoader(page);
        _loadedPages[page] = data.ToList();
        
        // 触发UI更新
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(
            NotifyCollectionChangedAction.Reset));
    }
    
    // 实现其他必要方法...
}

// 使用示例
public class DataService
{
    public async Task<IEnumerable<DeviceData>> GetDevicesAsync(int skip, int take)
    {
        // 模拟数据库查询
        await Task.Delay(100);
        
        return Enumerable.Range(skip, take)
            .Select(i => new DeviceData
            {
                DeviceName = $"Device_{i}",
                Temperature = 20 + i % 30
            });
    }
}

三、性能优化技巧

1. 绑定优化

csharp

// 1. 使用延迟绑定
public class DelayedBindingHelper
{
    public static readonly DependencyProperty DelayedTextProperty =
        DependencyProperty.RegisterAttached(
            "DelayedText",
            typeof(string),
            typeof(DelayedBindingHelper),
            new FrameworkPropertyMetadata(
                null,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                OnDelayedTextChanged,
                null,
                false,
                UpdateSourceTrigger.LostFocus));
    
    // 2. 绑定验证和转换优化
    public class TemperatureConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is double temp)
            {
                return $"{temp:F2} °C";
            }
            return string.Empty;
        }
        
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is string str && double.TryParse(str, out double temp))
            {
                return temp;
            }
            return 0.0;
        }
    }
}

2. 内存管理

csharp

public class MemoryOptimizedListBox : ListBox
{
    // 实现自定义虚拟化面板
    protected override void PrepareContainerForItemOverride(
        DependencyObject element, object item)
    {
        base.PrepareContainerForItemOverride(element, item);
        
        // 为可见项设置数据上下文
        if (element is FrameworkElement frameworkElement)
        {
            frameworkElement.DataContext = item;
        }
    }
    
    protected override void ClearContainerForItemOverride(
        DependencyObject element, object item)
    {
        // 清理资源
        if (element is FrameworkElement frameworkElement)
        {
            frameworkElement.DataContext = null;
        }
        
        base.ClearContainerForItemOverride(element, item);
    }
}

四、实际应用示例

监控系统示例

csharp

public class MonitoringSystem
{
    private readonly Timer _updateTimer;
    private readonly Random _random;
    private readonly DeviceViewModel _viewModel;
    
    public MonitoringSystem(DeviceViewModel viewModel)
    {
        _viewModel = viewModel;
        _random = new Random();
        _updateTimer = new Timer(UpdateData, null, 0, 1000);
    }
    
    private void UpdateData(object state)
    {
        // 模拟实时数据更新
        foreach (var device in _viewModel.Devices)
        {
            device.Temperature = 20 + _random.NextDouble() * 15;
            device.Timestamp = DateTime.Now;
        }
    }
    
    // 动态添加/移除设备
    public void AddNewDevice()
    {
        Application.Current.Dispatcher.Invoke(() =>
        {
            _viewModel.AddDevice($"设备_{Guid.NewGuid().ToString().Substring(0, 8)}");
        });
    }
}

配置虚拟化参数

xml

<!-- App.xaml 或 资源字典 -->
<Application.Resources>
    <Style TargetType="ListBox">
        <Setter Property="VirtualizingPanel.IsVirtualizing" Value="True"/>
        <Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling"/>
        <Setter Property="VirtualizingPanel.CacheLength" Value="50,50"/>
        <Setter Property="ScrollViewer.CanContentScroll" Value="True"/>
    </Style>
    
    <Style TargetType="DataGrid">
        <Setter Property="EnableRowVirtualization" Value="True"/>
        <Setter Property="EnableColumnVirtualization" Value="True"/>
        <Setter Property="RowHeight" Value="25"/>
        <Setter Property="VirtualizingPanel.ScrollUnit" Value="Pixel"/>
    </Style>
</Application.Resources>

五、最佳实践总结

  1. 动态绑定

    • 使用 ObservableCollection<T> 实现动态集合
    • 实现 INotifyPropertyChanged 实现属性变更通知
    • 使用 Dispatcher 在UI线程更新
  2. 虚拟化优化

    • 大量数据时启用虚拟化
    • 使用回收模式 (VirtualizationMode="Recycling")
    • 合理设置缓存长度
    • 避免嵌套虚拟化容器
  3. 性能监控

    csharp

    // 监控虚拟化性能
    PresentationTraceSources.DataBindingSource.Switch.Level = 
        SourceLevels.Warning | SourceLevels.Error;
    
  4. 内存优化

    • 及时释放不需要的绑定
    • 使用弱引用处理大型对象
    • 分页加载大数据集

这些技术结合使用,可以显著提升C#上位机应用程序的性能和响应速度,特别是在处理大量实时数据时。

救命!Python 这些基础操作居然能省一半工作量

2026年1月1日 14:48

作为一个常年和 Python 打交道的 “搬砖人”,我发现很多新手甚至老手,都会忽略一些基础但超实用的小技巧。明明一行代码能搞定的事,非要写个循环绕半天,属实是给自己加工作量了。

今天就来分享几个我日常高频使用的 Python 基础操作,简单好记,用起来是真的香!

1. 交换变量?不用临时变量也能行

新手交换两个变量,第一反应可能是定义一个临时变量中转:

a = 10
b = 20
# 新手写法
temp = a
a = b
b = temp

但 Python 里有更简洁的写法,直接一行搞定,逻辑还清晰:

a = 10
b = 20
# 简洁写法
a, b = b, a

不仅能交换两个变量,多个变量交换也同样适用,比如a, b, c = c, a, b,直接打乱顺序重新赋值,超方便。

2. 列表去重?别再写循环遍历了

面对一个有重复元素的列表,想快速去重,很多人会写个 for 循环,逐个判断是否在新列表里。

lst = [1, 2, 2, 3, 4, 4, 5]
new_lst = []
for i in lst:
    if i not in new_lst:
        new_lst.append(i)

但其实用 Python 的集合特性,一行就能去重,效率还更高:

lst = [1, 2, 2, 3, 4, 4, 5]
new_lst = list(set(lst))

不过要注意,集合是无序的,如果需要保持原列表的顺序,可以用dict.fromkeys(lst),再转成列表就行。

3. 快速拼接字符串?加号不如 join

拼接多个字符串时,用+号虽然直观,但效率很低,尤其是字符串数量多的时候。

str_list = ["我", "爱", "Python", "编程"]
result = ""
for s in str_list:
    result += s

推荐用join方法,不仅代码简洁,执行效率也提升不少:

str_list = ["我", "爱", "Python", "编程"]
result = "".join(str_list)

如果想给字符串之间加个分隔符,比如逗号,直接把引号里的内容改成,就行,","join(str_list)就能得到"我,爱,Python,编程"

4. 字典合并?三种方法任你选

日常开发中经常需要合并两个字典,新手可能会用循环逐个添加,其实 Python 有好几种简洁的写法。

dict1 = {"name": "张三", "age": 20}
dict2 = {"gender": "男", "city": "北京"}

# 方法1:用update方法
dict1.update(dict2)

# 方法2:用**解包
result = {**dict1, **dict2}

# 方法3:Python3.9+ 可用|运算符
result = dict1 | dict2

三种方法都能实现字典合并,需要注意的是,update会修改原字典,而另外两种方法会生成一个新字典,根据需求选择就行。

5. 快速生成列表?列表推导式 yyds

想根据一个列表生成新的列表,比如把所有元素乘以 2,新手可能会写循环:

lst = [1, 2, 3, 4, 5]
new_lst = []
for i in lst:
    new_lst.append(i * 2)

用列表推导式的话,一行就能搞定,代码更紧凑:

lst = [1, 2, 3, 4, 5]
new_lst = [i * 2 for i in lst]

还能加上条件判断,比如只生成偶数的倍数:[i * 2 for i in lst if i % 2 == 0],实用性拉满。

2025年度稀土掘金影响力榜单如约而至!

作者 掘金酱
2025年12月31日 15:57

稀土掘金社区2025年度影响力榜单正式公布

1303-734主视觉 (1) (3).jpg

时光向前,2025年即将落下句点。回首这一程,幸而有伴 —— 每一次深夜的打磨,每一段真诚的分享,每一回评论区里的倾力相助,都为技术探索的漫漫长路,点亮了一盏盏暖灯。

感谢这一年里,将实操经验凝注于代码、把深度思考落笔成文字的创作者。是每一个具体的 “你”,让稀土掘金始终活力满满。

今天,我们以这份榜单,记录那些值得被看见的光芒。它们来自团队扎实的沉淀,来自创作者持久的热情,也来自那些真正帮助过许多人的走心好文。

榜单地址aicoding.juejin.cn/pens/758993…

2025年度优秀创作者 | The Best 10 Creative Writers of 2025

他们是社区里的“解惑人”,把复杂讲得简单,把枯燥变得生动。他们的文字,曾陪伴无数掘友走出技术探索的迷茫时刻。

站内昵称 个人主页
Moment juejin.cn/user/378276…
ConardLi juejin.cn/user/394910…
何贤 juejin.cn/user/277499…
洛卡卡了 juejin.cn/user/888839…
why技术 juejin.cn/user/370281…
恋猫de小郭 juejin.cn/user/817692…
苏三说技术 juejin.cn/user/465848…
coder_pig juejin.cn/user/414261…
张风捷特烈 juejin.cn/user/149189…
德莱厄斯 juejin.cn/user/391911…

2025年度影响力团队 -The Most Influential Teams of 2025

他们以团队的力量,把一线实践沉淀成可复用的经验,如一张张清晰的技术地图,帮助不少同行找到了方向。

团队名称 团队主页
DevUI团队 juejin.cn/user/712139…
vivo互联网技术 juejin.cn/user/993614…
得物技术 juejin.cn/user/239295…
古茗前端团队 juejin.cn/user/323304…
货拉拉技术 juejin.cn/user/176848…
京东零售技术 juejin.cn/user/423357…
奇舞精选 juejin.cn/user/438890…
37手游后端团队 juejin.cn/user/154852…
哔哩哔哩技术 juejin.cn/user/303070…
转转技术团队 juejin.cn/user/606586…

2025年度爆款好文 | High hits articles in 2025

这20篇文章,是从海量分享中脱颖而出的“年度之选”。它们或视角新颖、或剖析深入、或实战性强,在各自领域内获得了认可,成了许多掘友收藏或推荐的那一篇。

文章标题 文章链接 所属作者
前端
《40岁老前端2025年上半年都学了什么?》 juejin.cn/post/752454… 张鑫旭
《前端仔如何在公司搭建 AI Review 系统》 juejin.cn/post/753259… 唐某人丶
《因为写了一个前端脚手架,这个月的KPI 打满了!!!》 juejin.cn/post/745793… 赵小川
《因网速太慢我把20M+的字体压缩到了几KB》 juejin.cn/post/749033… 古茗前端团队
《历经4个月,基于 Tiptap 和 NestJs 打造一款 AI 驱动的智能文档协作平台 🚀🚀🚀》 juejin.cn/post/755316… Moment
后端
《MCP 很火,来看看我们直接给后台管理系统上一个 MCP?》 juejin.cn/post/748149… Hamm
《还在用WebSocket实现即时通讯?试试MQTT吧,真香!》 juejin.cn/post/753935… MacroZheng
《我做了套小红书一键发布系统,运营小姐姐说她不想离开我了》 juejin.cn/post/755248… 洛卡卡了
《Java 实现责任链模式 + 策略模式:优雅处理多级请求的方式》 juejin.cn/post/745736… 后端出路在何方
《CompletableFuture还能这么玩》 juejin.cn/post/745556… 一只叫煤球的猫
移动端
《2025 跨平台框架更新和发布对比,这是你没看过的全新版本》 juejin.cn/post/750557… 恋猫de小郭
《月下载 40 万次的框架是怎么练成的?》 juejin.cn/post/754740… Android轮子哥
《[targetSDK升级为35] 恶心的EdgeToEdge适配 (v7)》 juejin.cn/post/749717… snwrking
《gson很好,但我劝你在Kotlin上使用kotlinx.serialization》 juejin.cn/post/745929… 沈剑心
《开箱即食Flutter通用脚手架》 juejin.cn/post/748278… SunshineBrother
人工智能
《一天 AI 搓出痛风伴侣 H5 程序,前后端+部署通吃,还接入了大模型接口(万字总结)》 juejin.cn/post/751749… 志辉AI编程
《AI 应用开发入门:前端也可以学习 AI》 juejin.cn/post/751719… 唐某人丶
《如何把你的 DeePseek-R1 微调为某个领域的专家?》 juejin.cn/post/747330… ConardLi
《3天,1人,从0到付费产品:AI时代个人开发者的生存指南》 juejin.cn/post/757765… HiStewie
《全网最细,一文带你弄懂 MCP 的核心原理!》 juejin.cn/post/749345… ConardLi

好的社区,是人与人相互照亮

这份榜单,与其说是评选,不如说是一次郑重的致谢。谢谢所有分享者,也谢谢每一位静静学习、默默点赞、热心评论的掘友。

技术之路,日常而长远。2026年,愿你继续在这里写下自己的章节,发出自己的光。我们相信,每一个人的微小光芒,终将汇聚成行业的星辰。

*注:以上排名不分先后,随机排序。本榜单依据2025年1月1日至12月21日期间的数据综合评定,涵盖文章质量、互动热度、创作者影响力、分类分布及评审团意见等多维度,最终解释权归稀土掘金所有。

Promise的总结

2026年1月1日 12:47

一、基本概念

  • Promise 是异步编程的解决方案,用于处理异步操作
  • 代表一个未来才会知道结果的事件(通常是异步操作)
  • 状态一旦改变就不会再变

二、三种状态

  1. pending(进行中)  - 初始状态
  2. fulfilled(已成功)  - 操作成功完成
  3. rejected(已失败)  - 操作失败

状态转换:
pending → fulfilled 或 pending → rejected

三、基本用法

javascript

const promise = new Promise((resolve, reject) => {
  // 异步操作
  if (/* 成功 */) {
    resolve(value);  // 状态变为fulfilled
  } else {
    reject(error);   // 状态变为rejected
  }
});

promise.then(
  value => { /* 成功处理 */ },
  error => { /* 失败处理 */ }
);

四、实例方法

1. then()  - 主要处理方法

javascript

promise.then(
  onFulfilled,  // 成功回调
  onRejected    // 失败回调(可选)
);

2. catch()  - 错误处理

javascript

promise.catch(error => {
  // 处理错误(等同于then(null, onRejected))
});

3. finally()  - 最终执行

javascript

promise.finally(() => {
  // 无论成功失败都会执行
});

五、静态方法

1. Promise.resolve()  - 创建已完成的Promise

javascript

Promise.resolve(value);

2. Promise.reject()  - 创建已拒绝的Promise

javascript

Promise.reject(reason);

3. Promise.all()  - 所有完成才完成

javascript

Promise.all([p1, p2, p3])
  .then(values => { /* 所有都成功 */ })
  .catch(error => { /* 有一个失败就失败 */ });

4. Promise.race()  - 竞速,第一个完成/拒绝的

javascript

Promise.race([p1, p2, p3])
  .then(value => { /* 第一个完成的 */ });

5. Promise.allSettled()  - 所有都完成(无论成功失败)

javascript

Promise.allSettled([p1, p2, p3])
  .then(results => { /* 所有promise都已完成 */ });

6. Promise.any()  - 任意一个成功就成功

javascript

Promise.any([p1, p2, p3])
  .then(value => { /* 第一个成功的 */ });

六、链式调用

javascript

promise
  .then(value => { /* 第一步 */ })
  .then(value => { /* 第二步 */ })
  .catch(error => { /* 错误处理 */ })
  .finally(() => { /* 清理 */ });

特点:

  • 每个then()返回新的Promise
  • 可以传递值给下一个then()
  • 错误会沿着链向后传递

七、错误处理最佳实践

javascript

// 推荐:使用catch处理错误
promise
  .then(handleSuccess)
  .catch(handleError);

// 避免:then中第二个参数和catch混用
promise
  .then(handleSuccess, handleError)  // 可能无法捕获then中的错误
  .catch(handleOtherError);           // 可能不会执行

八、与async/await结合

javascript

async function fetchData() {
  try {
    const result = await promise;
    // 处理结果
  } catch (error) {
    // 处理错误
  }
}

九、优缺点

优点:

  1. 解决回调地狱,代码更清晰
  2. 更好的错误处理机制
  3. 支持链式调用
  4. 提供了丰富的静态方法

缺点:

  1. 无法取消Promise
  2. 错误需要显式捕获
  3. 状态单一,不能监听进度

十、常见使用场景

  1. AJAX请求 - 替代XMLHttpRequest
  2. 定时器操作 - setTimeout封装
  3. 文件读取 - Node.js文件操作
  4. 数据库操作 - 异步查询
  5. 多个异步操作协调 - 使用Promise.all等

十一、注意事项

  1. Promise内部错误会被吞没,必须使用catch
  2. Promise创建即执行,不能中途取消
  3. 状态不可逆,一旦改变无法回退
  4. then()中的错误需要显式处理

十二、现代JavaScript实践

javascript

// ES2020+ 推荐写法
const fetchUser = async (userId) => {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) throw new Error('Network error');
    return await response.json();
  } catch (error) {
    console.error('Fetch failed:', error);
    throw error; // 重新抛出
  }
};

Promise是现代JavaScript异步编程的核心,虽然现在有async/await语法糖,但理解Promise是掌握JavaScript异步编程的基础。

用 Tauri 2.0 + React + Rust 打造跨平台文件工具箱

作者 借个火er
2026年1月1日 11:46

用 Tauri 2.0 + React + Rust 打造跨平台文件工具箱

前言

最近在整理电脑文件时,发现了大量重复的照片和视频,占用了几十 GB 的空间。市面上的去重工具要么收费,要么功能臃肿,于是萌生了自己造一个的想法。

正好 Tauri 2.0 正式发布,相比 Electron,它有着更小的包体积和更好的性能。于是决定用 Tauri 2.0 + React + Rust 来实现这个工具。

最终成品:File Toolkit —— 一个跨平台的文件工具箱,支持文件统计、文件去重、视频截取。

GitHub: github.com/220529/file…

功能展示

📊 文件统计

递归扫描文件夹,按类型统计文件数量和大小:

  • 支持拖拽选择文件夹
  • 按文件类型分组
  • 显示占比和总大小

🔍 文件去重

这是核心功能,支持:

  • 两阶段扫描:先按文件大小筛选,再计算哈希
  • xxHash3 快速哈希:比 MD5 快 5-10 倍
  • 并行计算:充分利用多核 CPU
  • 大文件采样:只读头部 + 中间 + 尾部,避免全量读取
  • 缩略图预览:图片直接显示,视频用 FFmpeg 截帧
  • 智能选择:自动选中较新的文件,保留最早的

✂️ 视频截取

  • 快速模式:无损截取(-c copy),秒级完成
  • 精确模式:重新编码,时间精确到毫秒
  • 时间轴预览:8 帧缩略图,快速定位
  • 实时进度:精确模式显示编码进度

技术选型

为什么选 Tauri 而不是 Electron?

对比项 Electron Tauri
包体积 150MB+ 10MB+
内存占用 高(Chromium) 低(系统 WebView)
后端语言 Node.js Rust
性能 一般 优秀

对于文件处理这种 CPU 密集型任务,Rust 的性能优势非常明显。

技术栈

┌─────────────────────────────────────────────────────┐
│                    Frontend                          │
│         React 19 + TypeScript + Tailwind CSS        │
├─────────────────────────────────────────────────────┤
│                   Tauri IPC                          │
├─────────────────────────────────────────────────────┤
│                    Backend                           │
│                  Rust + Tauri 2.0                   │
│  ┌─────────────┬─────────────┬─────────────────┐   │
│  │ file_stats  │    dedup    │     video       │   │
│  │  walkdir    │  xxHash3    │    FFmpeg       │   │
│  │             │  rayon      │                 │   │
│  │             │  memmap2    │                 │   │
│  └─────────────┴─────────────┴─────────────────┘   │
└─────────────────────────────────────────────────────┘

核心实现

1. 文件去重算法

去重的核心是计算文件哈希,但如果对每个文件都完整计算哈希,效率会很低。我采用了两阶段策略:

第一阶段:按文件大小筛选

let mut size_map: HashMap<u64, Vec<String>> = HashMap::new();

for entry in WalkDir::new(&path)
    .into_iter()
    .filter_map(|e| e.ok())
    .filter(|e| e.file_type().is_file())
{
    if let Ok(meta) = entry.metadata() {
        let size = meta.len();
        if size > 0 {
            size_map.entry(size).or_default().push(entry.path().to_string_lossy().to_string());
        }
    }
}

// 只对大小相同的文件计算哈希
let files_to_hash: Vec<_> = size_map
    .iter()
    .filter(|(_, files)| files.len() >= 2)
    .flat_map(|(size, files)| files.iter().map(move |f| (*size, f.clone())))
    .collect();

这一步可以过滤掉大部分文件,因为大小不同的文件肯定不重复。

第二阶段:并行计算哈希

use rayon::prelude::*;

let results: Vec<(String, FileInfo)> = files_to_hash
    .par_iter()  // 并行迭代
    .filter_map(|(size, file_path)| {
        let hash = calculate_fast_hash(Path::new(file_path), *size).ok()?;
        // ...
        Some((hash, file_info))
    })
    .collect();

使用 rayon 实现并行计算,充分利用多核 CPU。

2. 快速哈希算法

传统的 MD5 哈希速度较慢,我选择了 xxHash3,它是目前最快的非加密哈希算法之一:

fn calculate_fast_hash(path: &Path, size: u64) -> Result<String, String> {
    use memmap2::Mmap;
    use xxhash_rust::xxh3::Xxh3;

    const SMALL_FILE: u64 = 1024 * 1024;      // 1MB
    const THRESHOLD: u64 = 10 * 1024 * 1024;  // 10MB
    const SAMPLE_SIZE: usize = 1024 * 1024;   // 采样 1MB

    let file = File::open(path).map_err(|e| e.to_string())?;

    if size <= SMALL_FILE {
        // 小文件:内存映射,零拷贝
        let mmap = unsafe { Mmap::map(&file) }.map_err(|e| e.to_string())?;
        let hash = xxhash_rust::xxh3::xxh3_64(&mmap);
        return Ok(format!("{:016x}", hash));
    }

    let mut hasher = Xxh3::new();

    if size <= THRESHOLD {
        // 中等文件:完整读取
        let mmap = unsafe { Mmap::map(&file) }.map_err(|e| e.to_string())?;
        hasher.update(&mmap);
    } else {
        // 大文件:只读头部 + 中间 + 尾部
        let mmap = unsafe { Mmap::map(&file) }.map_err(|e| e.to_string())?;
        let len = mmap.len();

        hasher.update(&mmap[..SAMPLE_SIZE]);                    // 头部
        hasher.update(&mmap[len/2 - SAMPLE_SIZE/2..][..SAMPLE_SIZE]); // 中间
        hasher.update(&mmap[len - SAMPLE_SIZE..]);              // 尾部
        hasher.update(&size.to_le_bytes());                     // 文件大小
    }

    Ok(format!("{:016x}", hasher.digest()))
}

优化点

  • xxHash3:比 MD5 快 5-10 倍
  • memmap2:内存映射,零拷贝读取
  • 大文件采样:只读头中尾各 1MB,避免全量读取

3. 前后端通信

Tauri 使用 IPC 进行前后端通信。后端定义命令:

#[tauri::command]
pub async fn find_duplicates(app: AppHandle, path: String) -> Result<DedupResult, String> {
    // ...
}

前端调用:

import { invoke } from "@tauri-apps/api/core";

const result = await invoke<DedupResult>("find_duplicates", { path });

4. 进度反馈

长时间任务需要显示进度,Tauri 支持事件机制:

后端发送进度

use tauri::{AppHandle, Emitter};

let _ = app.emit("dedup-progress", DedupProgress {
    stage: "计算文件指纹".into(),
    current,
    total: total_to_hash,
    percent,
});

前端监听

import { listen } from "@tauri-apps/api/event";

useEffect(() => {
  const unlisten = listen<DedupProgress>("dedup-progress", (event) => {
    setProgress(event.payload);
  });
  return () => { unlisten.then((fn) => fn()); };
}, []);

5. 内嵌 FFmpeg

视频功能依赖 FFmpeg,为了让用户开箱即用,我把 FFmpeg 打包进了应用:

配置 tauri.conf.json

{
  "bundle": {
    "externalBin": [
      "binaries/ffmpeg",
      "binaries/ffprobe"
    ]
  }
}

Rust 中获取路径

fn get_ffmpeg_path(app: &AppHandle) -> PathBuf {
    app.path()
        .resource_dir()
        .ok()
        .map(|p| p.join("binaries").join("ffmpeg"))
        .filter(|p| p.exists())
        .unwrap_or_else(|| PathBuf::from("ffmpeg"))  // 回退到系统 PATH
}

踩坑记录

1. Tauri 拖拽事件是全局的

最初使用 CSS hidden 隐藏非活动 Tab 来保持状态,但发现拖拽文件时所有 Tab 都会响应。

解决方案:给每个组件传递 active 属性,只有激活的组件才监听拖拽事件。

useEffect(() => {
  if (!active) return;  // 非激活状态不监听
  
  const unlisten = listen("tauri://drag-drop", (event) => {
    if (!active) return;
    // 处理拖拽
  });
  // ...
}, [active]);

2. 并行计算进度跳动

使用 rayon 并行计算时,多线程同时更新计数器,导致进度显示不连续。

解决方案:使用原子变量 + compare_exchange 确保进度单调递增:

let progress_counter = Arc::new(AtomicUsize::new(0));
let last_reported = Arc::new(AtomicUsize::new(0));

// 在并行迭代中
let current = progress_counter.fetch_add(1, Ordering::Relaxed) + 1;
let last = last_reported.load(Ordering::Relaxed);

if current > last && (current - last >= 20 || current == total) {
    if last_reported.compare_exchange(last, current, Ordering::SeqCst, Ordering::Relaxed).is_ok() {
        // 发送进度
    }
}

3. Release 比 Debug 快很多

开发时觉得去重速度一般,打包后发现快了好几倍。

模式 说明
Debug 无优化,保留调试信息
Release LTO、内联、循环展开等优化

对于 CPU 密集型任务,Release 版本可能快 3-5 倍。

打包发布

本地打包

# 1. 下载 FFmpeg 静态版本(macOS 示例)
curl -L "https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip" -o /tmp/ffmpeg.zip
unzip /tmp/ffmpeg.zip -d src-tauri/binaries/
mv src-tauri/binaries/ffmpeg src-tauri/binaries/ffmpeg-x86_64-apple-darwin

# 2. 执行打包
pnpm tauri build

产物:src-tauri/target/release/bundle/macos/File Toolkit.app(约 62MB,含 FFmpeg)

GitHub Actions 多平台自动打包

本地打包只能生成当前平台的安装包。要支持 Windows、Linux,需要在对应平台编译。

解决方案:用 GitHub Actions,它提供 macOS、Windows、Linux 虚拟机,可以并行打包。

git push tag v0.2.0
        ↓
GitHub Actions 触发
        ↓
┌─────────────────────────────────────────────────────┐
│  同时启动 4 台虚拟机(并行执行)                       │
├─────────────┬─────────────┬────────────┬────────────┤
│  macOS VM   │  macOS VM   │ Windows VM │  Linux VM  │
│  (Intel)    │  (ARM)      │            │            │
├─────────────┼─────────────┼────────────┼────────────┤
│ 装环境      │ 装环境       │ 装环境     │ 装环境      │
│ 下载 FFmpeg │ 下载 FFmpeg │ 下载FFmpeg │ 下载FFmpeg │
│ tauri build │ tauri build │ tauri build│ tauri build│
├─────────────┼─────────────┼────────────┼────────────┤
│   .dmg.dmg.msi.deb     │
│  (x86_64)   │  (aarch64)  │   .exe.AppImage │
└─────────────┴─────────────┴────────────┴────────────┘
        ↓
   全部上传到 GitHub Release

核心配置 .github/workflows/release.yml

name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    strategy:
      matrix:
        include:
          - platform: macos-latest
            target: x86_64-apple-darwin
          - platform: macos-latest
            target: aarch64-apple-darwin
          - platform: windows-latest
            target: x86_64-pc-windows-msvc
          - platform: ubuntu-22.04
            target: x86_64-unknown-linux-gnu

    runs-on: ${{ matrix.platform }}

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - uses: pnpm/action-setup@v4
      - uses: dtolnay/rust-toolchain@stable

      # 各平台下载对应的 FFmpeg
      - name: Download FFmpeg (macOS)
        if: matrix.platform == 'macos-latest'
        run: |
          curl -L "https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip" -o /tmp/ffmpeg.zip
          # ...

      - name: Download FFmpeg (Windows)
        if: matrix.platform == 'windows-latest'
        shell: pwsh
        run: |
          Invoke-WebRequest -Uri "https://www.gyan.dev/ffmpeg/builds/..." -OutFile ...
          # ...

      # 打包并上传到 Release
      - uses: tauri-apps/tauri-action@v0
        with:
          tagName: ${{ github.ref_name }}
          releaseDraft: true
          args: --target ${{ matrix.target }}

使用方式

git tag v0.2.0
git push origin v0.2.0
# 自动触发,完成后去 Releases 页面发布

为什么能跨平台?

  • 前端:React 编译成 HTML/CSS/JS,哪都能跑
  • 后端:Rust 在目标平台的虚拟机上编译,生成原生二进制
  • FFmpeg:每个平台下载对应的静态版本

本质是在真实目标平台上编译,GitHub 免费提供这些虚拟机。

总结

这个项目让我对 Tauri 2.0 有了更深的理解:

  1. Rust 性能确实强:文件哈希、并行计算等场景优势明显
  2. Tauri 开发体验不错:前后端分离,IPC 通信简单
  3. 包体积小:不含 FFmpeg 只有 10MB 左右
  4. 跨平台:一套代码,多端运行

如果你也想尝试 Tauri,这个项目可以作为参考。

GitHub: github.com/220529/file…

欢迎 Star ⭐️


相关文章

TypeScript 架构实践:从后端接口到 UI 渲染数据流的完整方案

2026年1月1日 11:28

介绍在 TS 中 DTO 与 VO 的思想碰撞与实践,系统性地梳理从后端接口定义前端业务消费的完整链路。

一、 为什么要分 DTO 和 VO?

在 TypeScript 开发中,直接将后端返回的 JSON 数据透传到 UI 界面是研发初期的“捷径”,但往往也是后期维护的“噩梦”。为了解决后端字段多变、命名风格不统一(如蛇形命名 vs 驼峰命名)等问题,引入 DTO(数据传输对象)VO(视图对象) 的概念至关重要。

二、 核心思想:职责分离

我们将后端接口数据在系统中的流转拆分为三个关键环节:

环节 承载载体 核心职责
入口层 DTO (Interface) 契约。严格对齐接口协议,描述后端发来的原始数据。
转换层 Mapper 函数 解耦。负责逻辑清洗、字段重命名、类型转换。
应用层 VO (Interface) 纯净。仅包含 UI 层渲染所需的属性,命名符合前端规范。

三、 实战演练:Axios 与泛型的完美结合

为了实现自动化的类型推导,我们通过泛型封装通用的响应结构。

1. 定义通用响应壳子

// 统一后端返回的 JSON 格式
export interface ApiResponse<T = any> {
  code: number;
  message: string;
  data: T; // 这里的 T 将被具体的 DTO 替换
}

2. 定义 DTO 与 VO

利用 TS 工具类型(如 Pick)提高定义效率。

// 后端原始 DTO
export interface UserDTO {
  id: number;
  user_name: string;
  avatar_url: string;
  created_at: number; // 秒级时间戳
}

// 前端业务 VO
// 挑出需要的字段,并增加/修改业务字段
export type UserVO = Pick<UserDTO, 'id' | 'avatar_url'> & {
  displayName: string;
  regDate: Date; // 转换为 JS Date 对象
};

3. Axios 请求与 Mapper 转换

将 Axios 的泛型能力与转换函数串联起来:

import axios from 'axios';

axios.interceptors.response.use((response) => {
  const res = response.data as ApiResponse;
  if (res.code !== 200) {
    // 统一弹出后端给的错误提示
    showToast(res.message);
    return Promise.reject(new Error(res.message));
  }
  return response;
});

// 1. API 层
const fetchUserApi = (id: string) => {
  // 告知 Axios:返回值的 body 是 ApiResponse 结构,其 data 属性是 UserDTO
  return axios.get<ApiResponse<UserDTO>>(`/api/user/${id}`);
};

// 2. Mapper 层:负责 DTO -> VO 的脏活累活
const toUserVO = (dto: UserDTO): UserVO => ({
  id: dto.id,
  avatar_url: dto.avatar_url,
  displayName: dto.user_name || '未知用户',
  regDate: new Date(dto.created_at * 1000)
});

// 3. Service 层:业务组装
async function getUserDetail(id: string): Promise<UserVO> {
  const { data: res } = await fetchUserApi(id);
  // res.data 此时被自动推导为 UserDTO
  return toUserVO(res.data);
}

// 4. UI 层(Vue/React):直接使用 UserVO
const [user, setUser] = useState<UserVO | null>(null);

useEffect(() => {
  getUserDetail('123').then(setUser);
}, []);

总结

这套架构本质上是在前端构建了一道 “类型防火墙”

  • 防火墙外:是不可控的后端原始数据(DTO)。
  • 防火墙内:是稳定的、符合业务习惯的干净数据(VO)。
  • 防火墙中间:是透明的转换逻辑(Mapper)。

通过这种设计,即便后端接口字段发生变更,你只需修改 DTO 定义和 Mapper 函数,UI 层的代码完全不需要改动。

Next.js第十九章(服务器函数)

作者 小满zs
2026年1月1日 11:10

服务器函数(Server Actions)

服务器函数指的是可以是服务器组件处理表单的提交,无需手动编写API接口,并且还支持数据的验证,以及状态管理等。

核心原理

是因为React扩展了原生HTMLform表单,允许通过action属性直接绑定server action函数,当表单提交后,函数会自动接受原生的FormData数据。

基本用法

我们先回顾一下传统的表单提交方式:

sequenceDiagram
    participant U as 用户
    participant F as 前端表单
    participant A as API路由(route.ts)
    participant D as 数据库

    U->>F: 1. 填写表单数据<br/>{name: '张三', age: 18}
    U->>F: 2. 点击提交按钮
    F->>A: 3. fetch POST请求<br/>/api/xxx
    A->>A: 4. 解析请求数据
    A->>D: 5. 插入数据<br/>db.insert('user')
    D-->>A: 6. 返回结果
    A-->>F: 7. 响应 {code: 1, message: '成功'}
    F-->>U: 8. 显示结果

那么来看一下服务器函数的用法:

sequenceDiagram
    participant User as 用户
    participant Form as 表单组件
    participant ServerAction as handleLogin<br/>(服务器函数)
    participant DB as 数据库

    User->>Form: 填写用户名和密码
    User->>Form: 点击登录按钮(type="submit")
    Form->>ServerAction: 提交 FormData(action属性)
    Note over ServerAction: 'use server'<br/>在服务器端执行
    ServerAction->>ServerAction: 获取 username
    ServerAction->>ServerAction: 获取 password
    ServerAction->>ServerAction: 转换所有数据为对象
    ServerAction->>DB: 直接操作数据库<br/>(无需API接口)
    DB-->>ServerAction: 登录成功

src/app/login/page.tsx

export default function Login() {
    async function handleLogin(formData: FormData) {
        'use server'
        const username = formData.get('username') //接受单个参数
        const password = formData.get('password') //接受单个数据
        const form = Object.fromEntries(formData) //接受所有数据 {username: '张三', password: '123456'}
        //可以直接操作数据库,这样就无需编写API接口了 哇哦太方便了
    }
    return (
        <div>
            <h1>登录页面</h1>
            <div className="flex flex-col gap-2 w-[300px] mx-auto mt-30">
                <form action={handleLogin} className="flex flex-col gap-2">
                    <input className="border border-gray-300 rounded-md p-2" type="text" name="username" placeholder="用户名" />
                    <input className="border border-gray-300 rounded-md p-2" type="password" name="password" placeholder="密码" />
                    <button type="submit" className="bg-blue-500 text-white p-2 rounded-md">登录</button>
                </form>
            </div>
        </div>
    )
}

额外参数

目前只能携带固定参数例如 username password,无法携带其他参数。

<form action={handleLogin}>
    <input type="text" name="username" placeholder="用户名" />
    <input type="password" name="password" placeholder="密码" />
    <button type="submit">登录</button>
</form>

那么我想携带ID或者其他自定义参数怎么做?

我们需要使用bind方法来进行参数扩展,这样在函数内部就可以接收到ID参数。

export default function Login() {
                              //接受id参数
    async function handleLogin(id: number,formData: FormData) {
        'use server'
        const username = formData.get('username')
        const password = formData.get('password')
        const form = Object.fromEntries(formData)
        console.log(username, password,form,id)
    }
    const userFunction = handleLogin.bind(null,1) //绑定id参数
    return (
        <div>
            <h1>登录页面</h1>
            <div className="flex flex-col gap-2 w-[300px] mx-auto mt-30">
                        {/*使用新的函数绑定id参数 userFunction*/}
                <form action={userFunction} className="flex flex-col gap-2">
                    <input className="border border-gray-300 rounded-md p-2" type="text" name="username" placeholder="用户名" />
                    <input className="border border-gray-300 rounded-md p-2" type="password" name="password" placeholder="密码" />
                    <button type="submit" className="bg-blue-500 text-white p-2 rounded-md">登录</button>
                </form>
            </div>
        </div>
    )
}

参数校验(zod) + 读取状态

zod是一个目前非常流行的数据验证库,可以让我们在服务器端进行数据验证,避免用户输入非法数据。

npm i zod

src/app/lib/login/actions.ts

'use server'
import { z } from "zod"
const loginSchema = z.object({
    username: z.string().min(6, '用户名不能少于6位'), //zod基本用法表示这是一个字符串,并且不能少于6位
    password: z.string().min(6, '密码不能少于6位') //zod基本用法表示这是一个字符串,并且不能少于6位
})

export async function handleLogin(_prevState: any, formData: FormData) {
    const result = loginSchema.safeParse(Object.fromEntries(formData)) //调用zod的safeParse方法进行校验
    
    if (!result.success) {
        const errorMessage = z.treeifyError(result.error).properties; //调用zod的treeifyError方法将错误信息转换为对象
        let str = ''
        Object.entries(errorMessage!).forEach(([_key, value]) => {
            value.errors.forEach((error: any) => {
                str += error + '\n' //将错误信息拼接成字符串
            })
        })
        return { message: str} //返回错误信息
    }
    //校验成功,进行数据库操作逻辑
    return { message: '登录成功' } //返回成功信息
}

src/app/login/page.tsx

如果要读取状态需要使用React19的useActionState hook,这个hook必须在客户端组件中使用。所以需要增加'use client'声明这是一个客户端组件。

参数

useActionState hook接受三个参数:

  • fn: 表单提交时触发的函数,接收上一次的 state(首次为 initialState)作为第一个参数,其余参数为表单参数
  • initialState: state 的初始值,可以是任何可序列化的值
  • permalink(可选): 表单提交后跳转的 URL,用于 JavaScript 加载前的渐进式增强

返回值:

  • state: 当前状态,初始值为 initialState,之后为 action 的返回值
  • formAction: 新的 action 函数,用于传递给 form 或 button 组件
  • isPending: 布尔值,表示是否有正在进行的 Transition
'use client'
import { useActionState } from "react"
import { handleLogin } from "../lib/login/actions"
const initialState = { message:'' }
export default function Login() {
    const [state, formAction,isPending] = useActionState(handleLogin, initialState)
    
    return (
        <div>
            <h1>登录页面</h1>
            {isPending && <div>Loading...</div>}
            {state.message}
            <div className="flex flex-col gap-2 w-[300px] mx-auto mt-30">
                <form action={formAction} className="flex flex-col gap-2">
                    <input className="border border-gray-300 rounded-md p-2" type="text" name="username" placeholder="用户名" />
                    <input className="border border-gray-300 rounded-md p-2" type="password" name="password" placeholder="密码" />
                    <button type="submit" className="bg-blue-500 text-white p-2 rounded-md">登录</button>
                </form>
            </div>
        </div>
    )
}

🧠 从零开始:纯手写一个支持流式 JSON 解析的 React Renderer

作者 LeonGao
2026年1月1日 09:10

🌊 Part 1 — 为什么要支持流式 JSON?

想象一下:你有一个 10MB 的 JSON 文件,从网络上飞速传来。
如果你直接 JSON.parse() —— 嘭 💥 一下子内存吃光,还得等待整个文件下载完。

而我们聪明的做法是:
像看 Netflix 一样“边传边播”!
– JSON 数据一边传输,一边被解析,甚至可以边渲染 UI。

这就是所谓的 流式 JSON 解析 (Streaming JSON Parsing)


🔬 Part 2 — 架构拆解:我们想要什么东西?

我们像厨师一样备菜,先想清楚要做的菜系。

我们核心模块大概长这样👇:

+-----------------------------------------+
|          React Stream Renderer          |
+-----------------------------------------+
| 1️⃣ Stream Parser (逐字节喂食的JSON解析器) |
| 2️⃣ Fiber Scheduler (任务调度与更新队列)  |
| 3️⃣ Virtual Element Builder (将JSON转VNode)|
| 4️⃣ Host Renderer (将VNode渲染成DOM)      |
+-----------------------------------------+

简单概念对应表:

模块 功能
Stream Parser 流式解析 JSON 数据字符串
Fiber Scheduler 管理任务的优先级(模拟 React Fiber)
Virtual Element Builder 将 JSON 转换为 React Element 树
Host Renderer 将 Element 真正挂载到 DOM 上

✨ 我们今天的重点在于前两个模块 —— 流式解析 & 简单渲染调度。


🪄 Part 3 — 纯手写流式 JSON 解析器

我们不用 JSON.parse()
我们要用最“接地气”的方式:一个状态机 🧩。

思路如下:

  1. 不一次性读取所有数据;
  2. 每次只解析一小段;
  3. 当一个 JSON 对象完成后触发回调;
  4. 剩下的暂存在 buffer 中。

让我们硬核开干💪:

class StreamJSONParser {
  constructor(onObject) {
    this.buffer = '';
    this.depth = 0;
    this.inString = false;
    this.onObject = onObject;
  }

  feed(chunk) {
    for (const char of chunk) {
      this.buffer += char;

      if (char === '"' && this.buffer[this.buffer.length - 2] !== '\') {
        this.inString = !this.inString;
      }

      if (!this.inString) {
        if (char === '{' || char === '[') this.depth++;
        if (char === '}' || char === ']') this.depth--;
      }

      if (this.depth === 0 && !this.inString && this.buffer.trim()) {
        try {
          const obj = JSON.parse(this.buffer);
          this.onObject(obj);
          this.buffer = '';
        } catch (err) {
          // 未完成的 JSON,继续积累
        }
      }
    }
  }
}

🐍 解析逻辑的哲学可以一句话总结:

"不要急着吞数据,细嚼慢咽,待时机成熟,一口吞个键值对。"


🧩 Part 4 — JSON 到 React Element

我们定义一种简单的 JSON 协议:

{
  "type": "div",
  "props": {
    "className": "box"
  },
  "children": [
    {
      "type": "h1",
      "children": ["Hello Stream!"]
    }
  ]
}

接着写一个小的转换器:

function createElementFromJSON(node) {
  if (typeof node === 'string') return document.createTextNode(node);
  const el = document.createElement(node.type);
  if (node.props) {
    for (const key in node.props) {
      el[key] = node.props[key];
    }
  }
  if (node.children) {
    node.children.forEach(child => {
      el.appendChild(createElementFromJSON(child));
    });
  }
  return el;
}

这就像 React.createElement 的简陋山寨版,不过它完全服务于我们的小渲染器 👶。


⚡ Part 5 — 流式渲染管线!

现在我们可以把一切串起来了:

const root = document.getElementById('root');

const parser = new StreamJSONParser(obj => {
  const element = createElementFromJSON(obj);
  root.appendChild(element);
});

// 模拟网络流
const chunks = [
  '{"type":"div","children":["He',
  'llo "]}{"type":"p","children":["W',
  'orld"]}'
];

(async function streamFeed() {
  for (const chunk of chunks) {
    parser.feed(chunk);
    await new Promise(r => setTimeout(r, 500));
  }
})();

💡 每隔半秒喂一口,多么像 React 的 Suspense!这也是同一个思想的祖宗版本


🪶 Part 6 — Fiber? 调度? 随缘版 😄

当然,如果你真的想模仿 React Fiber,可以加一个“任务优先级队列”:

class MiniScheduler {
  constructor() {
    this.queue = [];
    this.working = false;
  }

  schedule(task) {
    this.queue.push(task);
    this.run();
  }

  async run() {
    if (this.working) return;
    this.working = true;
    while (this.queue.length) {
      const task = this.queue.shift();
      task();
      await new Promise(r => setTimeout(r)); // 模仿微任务
    }
    this.working = false;
  }
}

理论上你可以把 DOM 更新放进 schedule(),控制更新频率,从而实现“平滑的 UI 更新”🌈。


🎬 Part 7 — 总结 & 彩蛋

我们今天干了这些事:

模块 功能
StreamJSONParser 逐字节解析 JSON 流
Element Builder 从 JSON 创建 DOM
Renderer 组装 UI 管线
Scheduler 模拟 React Fiber 调度

哲学思考:

React 之所以强大,并不是因为它写了100万行代码,
而是因为它把“时间”和“空间”的问题拆开了处理。

你刚刚写的这几十行,就是 React 的灵魂缩影:
异步 + 流式 + Declarative + Incremental


🎉 彩蛋延伸练习:

  1. 让流式解析支持嵌套组件;
  2. 在渲染中加入虚拟节点 diff;
  3. 支持 Suspense:当解析到未完成的 JSON 节点时显示 Loading;
  4. 最后,可以考虑接入 WebSocket,让服务器实时推送虚拟节点!

👋 如果有一天,你在凌晨三点调试 Fiber 树,请记得这一句话:
我们不是在造轮子,我们在和轮子一起转动宇宙。 🌀💫


🧩 完整教学代码合集(适合实验):

class StreamJSONParser {
  constructor(onObject) {
    this.buffer = '';
    this.depth = 0;
    this.inString = false;
    this.onObject = onObject;
  }
  feed(chunk) {
    for (const char of chunk) {
      this.buffer += char;
      if (char === '"' && this.buffer[this.buffer.length - 2] !== '\') {
        this.inString = !this.inString;
      }
      if (!this.inString) {
        if (char === '{' || char === '[') this.depth++;
        if (char === '}' || char === ']') this.depth--;
      }
      if (this.depth === 0 && !this.inString && this.buffer.trim()) {
        try {
          const obj = JSON.parse(this.buffer);
          this.onObject(obj);
          this.buffer = '';
        } catch (err) {}
      }
    }
  }
}

function createElementFromJSON(node) {
  if (typeof node === 'string') return document.createTextNode(node);
  const el = document.createElement(node.type);
  if (node.props) {
    for (const key in node.props) el[key] = node.props[key];
  }
  if (node.children) {
    node.children.forEach(child => el.appendChild(createElementFromJSON(child)));
  }
  return el;
}

const root = document.getElementById('root');
const parser = new StreamJSONParser(obj => {
  const element = createElementFromJSON(obj);
  root.appendChild(element);
});

(async function simulateStream() {
  const chunks = [
    '{"type":"h1","children":["Stream JSON "]}',
    '{"type":"p","children":["Rendering live! 🚀"]}'
  ];
  for (const chunk of chunks) {
    parser.feed(chunk);
    await new Promise(r => setTimeout(r, 1000));
  }
})();

如果要设计一个开源的Code EditorSDK,你会向开发者暴露哪些API?

作者 海云前端1
2025年12月31日 21:55

设计一个开源的 Code Editor SDK(比如对标 Monaco、CodeMirror 6 或用于嵌入 AI 编程能力),核心目标是:让开发者能灵活集成、深度定制,又不至于被复杂性压垮

结合我在构建内部 Web IDE 和 AI 编码插件的经验,我会从 “基础能力 + 扩展机制 + AI 友好” 三个维度设计 API,以下是关键暴露点(真实可用、非理论):

一、核心编辑器控制 API

interface EditorSDK {
  // 1. 内容读写
  getValue(): string;
  setValue(code: string): void;
  getSelection(): SelectionRange;
  replaceSelection(replacement: string): void;

  // 2. 光标与位置
  getCursorPosition(): Position;
  setCursorPosition(pos: Position): void;

  // 3. 文件/语言管理
  setLanguage(lang: 'typescript' | 'python' | string): void;
  setFileName(name: string): void; // 影响 linting、completions

  // 4. 只读 / 可编辑控制
  setReadOnly(readonly: boolean): void;
}

为什么重要:这是所有上层功能(如 AI 生成、diff 应用)的基础原子操作。

二、事件系统

onDidChangeContent(callback: (e: { value: string }) => void): Disposable;
onDidChangeCursor(callback: (pos: Position) => void): Disposable;
onDidFocus(callback: () => void): Disposable;
onDidBlur(callback: () => void): Disposable;

💡 实战场景:AI 功能监听 onDidChangeContent 实现“实时解释当前代码”。

三、装饰与可视化扩展

// 1. 行内装饰(如 AI 建议、错误波浪线)
addDecoration(range: Range, options: {
  className?: string;
  tooltip?: string;
  inlineWidget?: ReactComponent; // 关键!支持嵌入按钮/加载态
}): string; // 返回 decorationId

removeDecoration(id: string): void;

// 2. 行号区域标记(如断点、Git diff)
setLineMarker(line: number, markerType: 'error' | 'warning' | 'ai-suggestion'): void;

真实用例:在 AI 生成代码上方插入 “💡 Apply” 按钮,点击后调用 replaceSelection

四、AI 友好型高级 API

// 1. 安全的代码补全注入(不污染原生 IntelliSense)
provideInlineCompletion(
  callback: (context: { prefix: string, suffix: string, language: string }) 
    => Promise<{ text: string, range?: Range }>
): Disposable;

// 2. 获取结构化上下文(供外部 LLM 使用)
getContext(): {
  fileName: string;
  language: string;
  code: string;
  cursorOffset: number;
  selectedText?: string;
  imports: string[]; // 通过轻量 AST 解析提取
};

// 3. 应用带差异的代码变更(安全合并)
applyDiff(diff: { start: Position, end: Position, newText: string }): void;

🔑 这些 API 让 AI 工具(如 Cursor 插件)无需侵入编辑器内核,就能实现智能补全、上下文感知生成。

五、扩展机制

registerCommand(id: string, handler: (...args) => void): void;
registerCompletionProvider(provider: CompletionProvider): Disposable;
registerHoverProvider(provider: HoverProvider): Disposable;

类似 VS Code 的 Extension API,但更轻量,适合 Web 场景。

六、性能与安全兜底

  • 沙箱模式enableSandbox() 禁用 eval、worker 等高危操作;
  • 资源控制setMaxFileSize(5 * 1024 * 1024) 防止大文件卡死;
  • Web Worker 支持:语法高亮、linting 默认走 worker,不阻塞主线程。

总结:

我会围绕 “可控、可观察、可扩展、AI-ready” 四个原则设计 API:

  • 暴露 内容、光标、语言 等基础操作;
  • 提供 事件监听 + 装饰系统 支持富交互;
  • 专为 AI 场景设计 上下文提取、安全补全、diff 应用 接口;
  • 通过 插件机制 允许深度定制,同时内置 性能与安全兜底

目标是:让开发者 5 分钟集成一个编辑器,5 天打造一个智能编程产品——这才是开源 SDK 的真正价值。

海云前端丨前端开发丨简历面试辅导丨求职陪跑

JavaScript 中的 this:作用域陷阱与绑定策略

作者 Zyx2007
2025年12月31日 21:46

在 JavaScript 编程中,this 是一个既强大又容易令人困惑的关键字。它的值并非由函数定义的位置决定,而是由函数调用的方式动态确定。这种灵活性带来了便利,也埋下了陷阱——尤其是在回调、定时器或事件处理等异步场景中,this 的指向常常“意外”地变成全局对象(如 window),导致方法调用失败或数据访问错误。理解其行为规律,并掌握正确的绑定技巧,是写出健壮代码的关键。

默认绑定:谁调用,this 就是谁

当一个函数作为对象的方法被调用时,this 自动指向该对象:

var a = {
  name: "Cherry",
  func1: function() {
    console.log(this.name); // "Cherry"
  }
};
a.func1(); // 调用者是 a,this 指向 a

这里,func1 通过 a.func1() 被调用,因此 this 绑定到 a,能正确访问其属性。这是最直观的 this 行为。

异步回调中的 this 丢失

问题常出现在将方法传入异步环境时。例如,在 setTimeout 中直接使用回调函数:

func2: function() {
  setTimeout(function() {
    this.func1(); // 报错!
  }, 1000);
}

尽管 func2a 的方法,但传给 setTimeout 的匿名函数是以普通函数形式执行的。在非严格模式下,其 this 指向全局对象 window,而 window 并无 func1 方法,程序因此崩溃。

三种主流解决方案

1. 显式绑定:使用 call / apply / bind

通过 callapply,可在调用时立即指定 this

setTimeout(function() {
  this.func1();
}.call(this), 1000);

这里,.call(this) 在定义回调的同时立即执行并绑定 this,但 setTimeout 实际接收的是函数的返回值(undefined),而非函数本身——此写法逻辑错误,无法实现延迟执行。正确做法应使用 bind

setTimeout(function() {
  this.func1();
}.bind(this), 1000);

bind 返回一个新函数,其 this 永久绑定到传入的对象,后续无论何处调用,this 都不会改变。

2. 闭包保存:that = this

在进入异步上下文前,将 this 赋值给一个变量(常命名为 thatself):

func2: function() {
  var that = this;
  setTimeout(function() {
    that.func1(); // 正确调用
  }, 1000);
}

由于 JavaScript 的词法作用域,内部函数能通过作用域链访问外层的 that,从而间接保留对原对象的引用。这是一种经典且兼容性极好的方案。

3. 箭头函数:继承父级 this

ES6 引入的箭头函数没有自己的 this,它会自动捕获定义时所在上下文的 this 值:

func2: function() {
  setTimeout(() => {
    this.func1(); // this 仍指向 a
  }, 1000);
}

箭头函数如同“懒人”,不创建独立的 this 绑定,而是沿用外层作用域的 this。在对象方法中使用箭头函数作为回调,能天然避免 this 丢失问题,代码也更简洁。

注意事项与适用场景

  • bind 适合需要多次调用或传递函数引用的场景,如事件监听器;
  • that = this 兼容旧环境,逻辑清晰,适合复杂嵌套;
  • 箭头函数简洁高效,但不可用于需要动态 this 的场合(如构造函数或需被 call 动态绑定的方法)。

此外,需注意 new 调用会创建全新对象并绑定 this,与上述规则无关。

结语

this 的动态绑定机制是 JavaScript 语言的重要特性,也是初学者常踩的“坑”。理解其在不同调用场景下的行为,并灵活运用 bind、闭包变量或箭头函数进行控制,不仅能避免运行时错误,还能提升代码的可读性与可靠性。掌握这些技巧,意味着你已迈出了从“能运行”到“写得好”的关键一步。

❌
❌