阅读视图

发现新文章,点击刷新页面。

【swift开发基础】34丨访问和操作数组:查找操作

一、访问和操作数组

1. 在末尾添加

  • 可变性要求:数组必须是变量(用var定义),常量数组不可修改
  • append方法:
    • append(:):在数组末尾添加单个元素
    • append(contentsOf:):在末尾添加多个元素(接受序列)
  • 示例:
    • numbers.append(100):在[2,3,4,5,6,7]后添加100 → [2,3,4,5,6,7,100]
    • numbers.append(contentsOf: 100...105):添加100到105区间 → [2,3,4,5,6,7,100,101,102,103,104,105]

2. 在任意位置添加

  • insert方法:
    • insert(_:at:):在指定位置插入单个元素
    • insert(contentsOf:at:):在指定位置插入多个元素
  • 示例:
    • numbers.insert(-1, at: 0):在0位置插入-1 → [-1,2,3,4,5,6,7]
    • numbers.insert(contentsOf: -2...0, at: 0):在0位置插入-2到0 → [-2,-1,0,2,3,4,5,6,7]

3. 字符串也是Collection

  • 特性:字符串是Character类型的集合,可直接作为内容插入字符数组

  • 示例:

    • chars.insert(contentsOf: "hello", at: 0):将"hello"分解为字符插入 → ["h","e","l","l","o","a","b","c"]

4. 移除单个元素

  • remove(at:):移除并返回指定位置元素(非空数组必须)

  • removeFirst():移除并返回首元素(数组为空会崩溃)

  • popFirst():安全移除首元素(返回Optional,数组为空返回nil)

  • 示例:

    • chars.remove(at: 1):移除索引1的元素"b" → ["a","c","d"]
    • chars.removeFirst():移除"a" → ["b","c","d"]
    • chars.popFirst():安全移除首元素(推荐使用)

5. 移除多个元素

  • 带参数版本:

    • removeFirst(_:):移除前N个元素
    • removeLast(_:):移除后N个元素
  • 示例:

    • chars.removeFirst(2):移除前两个 → ["c","d"]
    • chars.removeLast(2):移除后两个 → ["a","b"]

6. 移除多个元素

  • removeSubrange(_:):移除指定范围内的元素

  • removeAll():清空数组(释放内存)

  • removeAll(keepingCapacity:):清空但保留容量

  • 容量优化:建议后续需要插入操作时使用keepingCapacity:true

  • 示例:

    • chars.removeSubrange(1...2):移除索引1-2 → ["a","d"]
    • chars.removeAll(keepingCapacity: true):清空但容量保持4

7. 应用案例

1)例题:数组的添加和删除操作
  • 实践要点:

    • 添加操作必须使用变量数组
    • popLast()比removeLast()更安全
    • 批量操作时注意索引范围有效性
    • 频繁增删时建议使用keepingCapacity保持性能

二、知识小结

知识点 核心内容 易混淆点 难度系数
Swift数组添加元素 使用append在末尾添加单个元素,append(contentsOf:)添加多个元素 必须使用var定义可变数组 ⭐⭐
Swift数组插入元素 使用insert(_:at:)在指定位置插入单个元素,insert(contentsOf:at:)插入多个元素 插入位置索引越界会引发运行时错误 ⭐⭐⭐
Swift数组移除元素 remove(at:)移除指定位置元素,removeFirst()/removeLast()移除首尾元素,popLast()安全移除尾元素 removeFirst/removeLast在空数组调用会崩溃,而popLast返回可选值 ⭐⭐⭐⭐
Swift数组批量移除 removeSubrange(_:)移除范围元素,removeAll(keepingCapacity:)保留容量清空数组 keepingCapacity参数对性能优化的作用 ⭐⭐⭐
OC与Swift数组对比 OC中NSArray不可变,只有NSMutableArray可修改;Swift用let/var区分 OC需要显式使用可变类,Swift通过变量声明控制 ⭐⭐
字符串作为集合处理 字符串可分解为字符插入字符数组,体现Swift的Collection协议统一性 字符串与字符数组的类型转换机制 ⭐⭐⭐

Logoly:几秒钟做出「P站 风格」Logo 的开源小工具

很多人做个人主页、公众号封面、技术分享 PPT,都会遇到一个共同问题:
想要一个眼前一亮的 Logo,但又不会设计,也懒得打开 PS / Figma。

Logoly 就是为这种场景而生的一个开源项目:
一个可以在线生成 Pornhub / OnlyFans 风格 Logo 的小工具,只需要改几个字、调调颜色,几秒钟就能导出一张“似曾相识”的趣味 Logo。

👁️‍🗨️ Logoly 是什么?

Logoly 的作者给它的定义是:

A Simple Online Logo Generator for People Who Want to Design Logos Easily.
—— 让任何人都能轻松做 Logo 的在线生成器。

它最早因为可以生成类似 p站 风格的 Logo 在社区火了一把,后来又加入了 OnlyFans 风格的样式,逐渐变成一个 “恶搞 / 表情包 / 个人小品牌” 都能用的小工具。

特点概括一下:

  • 在线使用,无需安装,打开网页就能玩
  • 生成 P站 / OnlyFans 风格的 Logo
  • 支持自定义文字内容
  • 支持自定义颜色和字体大小
  • 一键导出 PNG / SVG 两种格式
  • 开源,基于 WTFPL 2 授权,几乎「想怎么玩就怎么玩」

github地址: github.com/bestony/log… 在线体验:www.logoly.pro/

该项目目前在github 上已有 7.9k ⭐️star

🎯 功能一览

1. 两种经典风格:P站风格 & OnlyFans

你可以一键做出这两种在互联网上辨识度极高的 Logo 风格,用来做:

  • 朋友圈/微博的整活配图
  • 技术分享 PPT 中的彩蛋
  • 团队内部的梗图或文化海报
  • 个人主页 / 博客上的趣味 Logo 等

风格本身非常强烈,但文字是你自定义的,可以天马行空。

注意:Logoly 本身不涉及任何成人内容,只是借鉴了这些品牌 Logo 的视觉风格。

2. 自定义文字内容

页面中有一个文本输入框,只需要:

  1. 把默认的文字改成你想要的(英文、数字、甚至中文都可以尝试)
  2. 实时预览 Logo 效果

很适合搞一些有梗、容易被记住的短词。

3. 自定义颜色和字体大小

你可以自由调整:

  • 左右两段文字的颜色
  • 中间色块的背景色(具体视当前主题而定)
  • 整体字号大小,用来适配不同场景(比如头像 vs 横幅)

简单几下滑动/点选,就能做出风格迥异的变体。

4. 导出 PNG / SVG

完成之后,点击 Export 按钮可以导出两种格式:

  • PNG:适合直接作为图片使用(社交头像、封面图、PPT、聊天表情等)
  • SVG:矢量格式,放大不会糊,适合用在网页、印刷、再加工设计中

对于前端开发者来说,SVG 的可编辑性也很友好,可以进一步嵌入到自己的项目里。

在线使用教程:4 步搞定一个 Logo

作者已经把使用流程总结成 4 步,实际体验下来也确实非常简单:

  1. 打开网站:
    访问 logoly.pro/

  2. 编辑文字:
    把默认的文本替换成你想要的单词/短句

  3. 调整样式:

    • 修改颜色
    • 调整字体大小
  4. 导出图片:
    点击 Export 按钮,选择 PNG 或 SVG 导出

整个过程基本不需要学习成本,几乎是「一看就会」。

以下是我生成的一些案例

✏️ 面向开发者:如何在本地运行 Logoly?

环境要求

项目对 Node / npm 版本有明确要求:

  • Node.js 18+
  • npm 10+
    (官方说明:只支持 npm 作为包管理器,请不要提交其他管理器生成的 lockfile)

也就是说:

  • 不建议使用 yarn / pnpm / Bun 来跑这个项目,以免锁文件不一致、CI 失败。
  • CI 流水线里也是基于 npm 的脚本执行。

本地跑起来的步骤

在命令行中依次执行:

# 1. 克隆仓库
git clone https://github.com/bestony/logoly.git
cd logoly

# 2. 安装依赖
npm install

# 3. 启动开发服务器
npm run dev

启动成功后,在浏览器里访问提示的本地地址(通常是 http://localhost:xxxx),就能看到 Logoly 的开发版界面。

修改源码后即可实时看到效果。
开发完成后,可以进行构建:

npm run build

docker部署

如果你想使用docker部署,则只需要启动一个nginx 容器,将构建后的dist目录下的文件及文件夹挂载到容器中即可,不会部署nginx的家人们可以搜索下博主的历史文章,有介绍docker 部署nginx 及配置的博文。

🦆 总结

如果你:

  • 需要一个有辨识度的 Logo,又不想折腾复杂设计工具
  • 想给项目、团队或朋友做一个带梗的图标
  • 想找一个小而美的前端开源项目学习 / 贡献

那么 Logoly 非常值得收藏一下。

你可以先用线上版做几个 Logo 玩玩。

解锁Vue新姿势:5种定义全局方法的实用技巧,让你的代码更优雅!

解锁Vue新姿势:5种定义全局方法的实用技巧,让你的代码更优雅!

无论你是Vue新手还是有一定经验的开发者,相信在工作中都遇到过这样的场景:多个组件需要用到同一个工具函数,比如格式化日期、权限验证、HTTP请求等。如果每个组件都单独引入,不仅代码冗余,维护起来也让人头疼。

今天我就为大家分享5种定义全局方法的实用方案,让你轻松解决这个问题!

🤔 为什么需要全局方法?

先来看一个真实的例子。假设你的项目中有三个组件都需要格式化日期:

// UserProfile.vue
methods: {
  formatDate(date) {
    return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
  }
}

// OrderList.vue  
methods: {
  formatDate(date) {
    return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
  }
}

// Dashboard.vue
methods: {
  formatDate(date) {
    return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
  }
}

发现了问题吗?同样的代码写了三遍!  这就是我们需要全局方法的原因。

📝 方案一:Vue.prototype(最经典的方式)

这是Vue 2时代最常用的方法,直接扩展Vue的原型链:

// main.js 或 plugins/global.js
import Vue from 'vue'

// 定义全局方法
Vue.prototype.$formatDate = function(date) {
  const dayjs = require('dayjs')
  return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
}

Vue.prototype.$checkPermission = function(permission) {
  const user = this.$store.state.user
  return user.permissions.includes(permission)
}

// 在组件中使用
export default {
  mounted() {
    console.log(this.$formatDate(new Date()))
    if (this.$checkPermission('admin')) {
      // 执行管理员操作
    }
  }
}

优点:

  • • 使用简单,直接通过 this 调用
  • • 广泛支持,兼容性好

缺点:

  • • 污染Vue原型链
  • • 方法多了难以管理
  • • TypeScript支持需要额外声明

🎯 方案二:全局混入(适合通用逻辑)

如果你有一组相关的全局方法,可以考虑使用混入:

// mixins/globalMethods.js
export default {
  methods: {
    $showSuccess(message) {
      this.$message.success(message)
    },
    $showError(error) {
      this.$message.error(error.message || '操作失败')
    },
    $confirmAction(title, content) {
      return this.$confirm(content, title, {
        type'warning'
      })
    }
  }
}

// main.js
import Vue from 'vue'
import GlobalMixin from './mixins/globalMethods'

Vue.mixin(GlobalMixin)

// 组件中使用
export default {
  methods: {
    async deleteItem() {
      try {
        await this.$confirmAction('确认删除''确定删除该记录吗?')
        await api.deleteItem(this.id)
        this.$showSuccess('删除成功')
      } catch (error) {
        this.$showError(error)
      }
    }
  }
}

适合场景:  UI反馈、确认对话框等通用交互逻辑。

🏗️ 方案三:独立模块 + Provide/Inject(Vue 3推荐)

Vue 3提供了更优雅的解决方案:

// utils/globalMethods.js
export const globalMethods = {
  // 防抖函数
  debounce(fn, delay = 300) {
    let timer = null
    return function(...args) {
      if (timer) clearTimeout(timer)
      timer = setTimeout(() => {
        fn.apply(this, args)
      }, delay)
    }
  },
  
  // 深度拷贝
  deepClone(obj) {
    return JSON.parse(JSON.stringify(obj))
  },
  
  // 生成唯一ID
  generateId() {
    return Math.random().toString(36).substr(29)
  }
}

// main.js
import { createApp } from 'vue'
import { globalMethods } from './utils/globalMethods'

const app = createApp(App)

// 通过provide提供给所有组件
app.provide('$global', globalMethods)

// 组件中使用
import { inject } from 'vue'

export default {
  setup() {
    const $global = inject('$global')
    
    const handleInput = $global.debounce((value) => {
      console.log('搜索:', value)
    }, 500)
    
    return { handleInput }
  }
}

这是Vue 3的推荐方式,保持了良好的类型推断和代码组织。

📦 方案四:插件化封装(企业级方案)

对于大型项目,建议采用插件化的方式:

// plugins/globalMethods.js
const GlobalMethodsPlugin = {
  install(app, options) {
    // 添加全局方法
    app.config.globalProperties.$http = async (url, config) => {
      try {
        const response = await fetch(url, config)
        return await response.json()
      } catch (error) {
        console.error('请求失败:', error)
        throw error
      }
    }
    
    app.config.globalProperties.$validate = {
      email(email) {
        return /^[^\s@]+@[^\s@]+.[^\s@]+$/.test(email)
      },
      phone(phone) {
        return /^1[3-9]\d{9}$/.test(phone)
      }
    }
    
    // 添加全局属性
    app.config.globalProperties.$appName = options?.appName || 'My App'
    
    // 添加自定义指令
    app.directive('focus', {
      mounted(el) {
        el.focus()
      }
    })
  }
}

// main.js
import { createApp } from 'vue'
import GlobalMethodsPlugin from './plugins/globalMethods'

const app = createApp(App)
app.use(GlobalMethodsPlugin, {
  appName'企业管理系统'
})

// 组件中使用
export default {
  mounted() {
    // 使用全局方法
    this.$http('/api/users')
    
    // 使用验证
    if (this.$validate.email(this.email)) {
      // 邮箱有效
    }
    
    // 访问全局属性
    console.log('应用名称:'this.$appName)
  }
}

🌟 方案五:Composition API方式(最现代)

如果你使用Vue 3的Composition API,可以这样组织:

// composables/useGlobalMethods.js
import { readonly } from 'vue'

export function useGlobalMethods() {
  // 定义所有全局方法
  const methods = {
    // 金额格式化
    formatCurrency(amount) {
      return '¥' + Number(amount).toFixed(2)
    },
    
    // 文件大小格式化
    formatFileSize(bytes) {
      const units = ['B''KB''MB''GB']
      let size = bytes
      let unitIndex = 0
      
      while (size >= 1024 && unitIndex < units.length - 1) {
        size /= 1024
        unitIndex++
      }
      
      return `${size.toFixed(1)} ${units[unitIndex]}`
    },
    
    // 复制到剪贴板
    async copyToClipboard(text) {
      try {
        await navigator.clipboard.writeText(text)
        return true
      } catch {
        // 降级方案
        const textArea = document.createElement('textarea')
        textArea.value = text
        document.body.appendChild(textArea)
        textArea.select()
        document.execCommand('copy')
        document.body.removeChild(textArea)
        return true
      }
    }
  }
  
  return readonly(methods)
}

// main.js
import { createApp } from 'vue'
import { useGlobalMethods } from './composables/useGlobalMethods'

const app = createApp(App)

// 挂载到全局
app.config.globalProperties.$globalMethods = useGlobalMethods()

// 组件中使用
import { getCurrentInstance } from 'vue'

export default {
  setup() {
    const instance = getCurrentInstance()
    const $global = instance?.appContext.config.globalProperties.$globalMethods
    
    // 或者在setup中直接引入
    // const $global = useGlobalMethods()
    
    return { $global }
  },
  mounted() {
    console.log(this.$global.formatCurrency(1234.56))
  }
}

📊 5种方案对比总结

方案 适用版本 优点 缺点 推荐指数
Vue.prototype Vue 2 简单直接 污染原型链 ⭐⭐⭐
全局混入 Vue 2/3 逻辑分组 可能造成冲突 ⭐⭐⭐
Provide/Inject Vue 3 类型安全 使用稍复杂 ⭐⭐⭐⭐
插件封装 Vue 2/3 功能完整 配置复杂 ⭐⭐⭐⭐⭐
Composition API Vue 3 现代灵活 需要Vue 3 ⭐⭐⭐⭐⭐

💡 最佳实践建议

  1. 1. 按功能分类组织
// 不推荐:把所有方法堆在一个文件
// 推荐:按功能模块拆分
utils/
  ├── formatters/    # 格式化相关
  ├── validators/    # 验证相关  
  ├── http/         # 请求相关
  └── ui/           # UI交互相关
  1. 2. 添加TypeScript支持
// global.d.ts
declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $formatDate(date: Date) => string
    $checkPermission(permission: string) => boolean
  }
}
  1. 3. 注意性能影响
  • • 避免在全局方法中执行重逻辑
  • • 考虑使用懒加载
  • • 及时清理不再使用的方法
  1. 4. 保持方法纯净
  • • 一个方法只做一件事
  • • 做好错误处理
  • • 添加详细的JSDoc注释

🎁 福利:一个实用的全局方法库

我整理了一些常用的全局方法,你可以直接使用:

// utils/essentials.js
export const essentials = {
  // 下载文件
  downloadFile(url, filename) {
    const link = document.createElement('a')
    link.href = url
    link.download = filename
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
  },
  
  // 获取URL参数
  getUrlParam(name) {
    const params = new URLSearchParams(window.location.search)
    return params.get(name)
  },
  
  // 休眠函数
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms))
  },
  
  // 对象转FormData
  objectToFormData(obj) {
    const formData = new FormData()
    Object.keys(obj).forEach(key => {
      formData.append(key, obj[key])
    })
    return formData
  }
}

✨ 结语

掌握全局方法的定义和使用,能够让你的Vue项目更加模块化、可维护、高效。不同的方案适用于不同的场景和需求,关键是要根据项目实际情况选择最合适的方式。

记住:好的代码不是写出来的,而是设计出来的。

希望今天的分享对你有帮助!如果你有更好的方案或实践经验,欢迎在评论区留言分享。

详细说一下nuxt generate是干啥的

nuxt generate 是 Nuxt.js 框架的一个**静态站点生成(Static Site Generation, SSG)**命令。我来详细解释一下它的含义和作用:

是什么?

nuxt generate 命令会将你的 Nuxt 应用预渲染为静态 HTML 文件。它会:

  • 为每个路由生成对应的 HTML 文件
  • 将生成的静态文件保存在 dist/ 目录中
  • 包含必要的 CSS、JavaScript 和资源文件

主要作用

1. 性能优化

  • 预生成的 HTML 文件无需服务器端渲染,加载速度极快
  • CDN 友好,可以轻松缓存
  • 减少服务器压力和响应时间

2. SEO 优化

  • 搜索引擎可以直接抓取静态 HTML 内容
  • 更好的 SEO 表现(相比于纯客户端渲染)

3. 部署简单

  • 生成的文件可以部署到任何静态主机:
    • Netlify、Vercel、GitHub Pages
    • AWS S3、Firebase Hosting
    • Nginx、Apache 等传统服务器

4. 成本效益

  • 无需专门的 Node.js 服务器
  • 可以使用廉价的静态托管服务

使用场景

适合使用 nuxt generate

内容型网站:博客、文档、营销页面
数据不频繁变化:产品展示页、公司官网
需要优秀 SEO 的应用
高访问量 的只读页面

不适合使用(需要考虑 SSR 或 CSR):

用户个性化内容:每个用户看到的内容不同
实时数据:股票行情、聊天应用
频繁更新:社交媒体动态
需要身份验证 的页面(可通过混合模式解决)

基本使用

# 生成静态文件
nuxt generate

# 生成后预览
nuxt generate && nuxt start

# 构建并生成(常用)
npm run generate
# 在 package.json 中通常配置为:
# "scripts": {
#   "generate": "nuxt generate"
# }

配置示例

// nuxt.config.js
export default {
  target: 'static', // 明确指定为静态站点
  generate: {
    // 动态路由需要指定
    routes: [
      '/users/1',
      '/users/2',
      '/blog/post-1'
    ],
    // 或者异步获取路由
    async routes() {
      const posts = await $fetch('/api/posts')
      return posts.map(post => `/blog/${post.id}`)
    }
  }
}

工作流程

执行 nuxt generate
    ↓
Nuxt 启动构建过程
    ↓
为每个路由生成 HTML
    ↓
提取 CSS 和 JavaScript
    ↓
保存到 dist/ 目录
    ↓
完成!可以部署到任何静态主机

nuxt build 的区别

  • nuxt generate:生成静态 HTML 文件,用于静态托管
  • nuxt build:构建应用,用于服务器端渲染(SSR)部署

高级特性

1. 混合模式

// 部分页面静态生成,部分页面动态渲染
export default {
  generate: {
    exclude: [
      '/dashboard',  // 这个页面保持动态
      '/admin/**'    // 所有 admin 页面都动态
    ]
  }
}

2. 增量静态再生

可以通过定时任务重新生成部分页面。

实际示例

# 1. 创建 Nuxt 项目
npx nuxi@latest init my-static-site

# 2. 安装依赖
cd my-static-site
npm install

# 3. 生成静态文件
npm run generate

# 4. 查看生成的文件
ls -la dist/
# 会看到 index.html, about.html 等

# 5. 本地测试生成的文件
npx serve dist/

总之,nuxt generate 是 Nuxt.js 强大的静态站点生成功能,特别适合需要优秀性能、SEO 和低成本部署的场景。对于适合静态化的项目,它能提供极佳的用户体验。

小程序增加用户协议

在小程序中增加一个用户协议

1.开发用户协议页面

在一个内部网站上开发一个用户协议的页面。

2.在小程序开发者后台添加业务域名

添加的时候,会给一个验证码文件。下载下来。

3. 将验证码文件放在用户协议所在网站的根目录

4. 在小程序中使用webview加载网页链接

<template>
  <view class="settings-page">
    <view class="list-card">
      <view
        class="list-item"
        @tap="openPage(userAgreementUrl)"
      >
        <text>用户协议</text>
        <view class="arrow" />
      </view>
    </view>
  </view>
  <view>
    <web-view
        v-if="userAgreementUrl"
        :src="userAgreementUrl"
    ></web-view>
</template>

<script>
import Taro from '@tarojs/taro'
import { USER_AGREEMENT_URL } from '@/config/legal'

export default {
  name: 'settings-page',
  data() {
    return {
      userAgreementUrl: USER_AGREEMENT_URL,
    }
  },
  methods: {
    openPage(url) {
      if (!url) {
        Taro.showToast({
          title: '链接未配置',
          icon: 'none'
        })
        return
      }
      const target = encodeURIComponent(url)
      Taro.navigateTo({
        url: `/pages/webview/webview?url=${target}`
      })
    }
  }
}
</script>

其中legal文件代码如下:

export const USER_AGREEMENT_URL = 'https:/XXXXXXX/privacy/user.html'

如何用一个 mcp 来教笨蛋 AI 好好干活

我们在写伪代码吗?

相信很多人都有这种感觉:写 Prompt 越来越像在写伪代码

  • “先 xxx,再 xxx” → 对应代码的执行顺序
  • “如果 xxx,就 xxx” → if 逻辑
  • “一直执行直到 xxx” → while 循环

既然如此,我们能不能直接把一段“伪代码”丢给 AI?

kimi-k2 执行效果: image.png

看起来还可以!

给它加一点限制,让它从 1+...+ 到 n,同时排除所有位数之和加起来为 5 的倍数的数字,不能调用脚本:

let count = Tool.AskUserQuestion(`please input a number`);
let sum = 0;
for (let i = 1; i <= count; i++) {
  if (Prompt(`${i} 的每个位数加起来之和为 5 的倍数`)) {

  } else {
    sum += i;
  }
}
Prompt(`最终计算结果为 ${sum}`)

image.png

好吧,这下原型毕露了,比如 86 这个完全不应该排除的数字,被它直接排除掉了。

而且它是先计算的所有值的和,再减去需要排除的值,其实没有严格按照我们的逻辑来执行。

其实好好想一想上面的过程,我们把一个伪代码丢给大模型来执行,期望于就像把代码丢给编译器来执行一样,但是 AI 有着很多的幻觉,这个“编译器”很不稳定。

在一些复杂任务的实测里,它会跳过逻辑胡乱执行,比如没按照预期调用 tool,或者直接在半路上认为自己已经成功了,特别是一些笨蛋 AI,每次执行过程和结果可能都不一样。

可以用代码驱动 AI 吗?

为了解决这种不稳定性,我们需要一种能强约束执行流程的工具。

在 Claude Code 或类似的 Agent 框架中,AI 可以根据 Tool 的返回决定下一步。那么,我们能不能反过来?由一段真实的代码来驱动 AI,AI 只负责完成其中的“自然语言函数”部分?

这正是 agent-workflow-mcp-tool 的核心思路:利用 MCP (Model Context Protocol) 协议,通过 TypeScript 的 Generator 函数,将 AI 变成流程中的一个执行单元。

下面的代码是可以真实执行的代码而非伪代码:

async function* Workflow() {
  const count = yield* ClaudeCodeTools.AskUserQuestion(
    `please input a number`,
    z.number()
  );

  let sum = 0;
  for (let i = 1; i <= count; i++) {
    sum = yield* Prompt(`calculate ${sum} + ${i}`, z.number());
  }
  return sum;
}

github 地址:github.com/voderl/agen…

它有哪些优势呢:

  • 用代码控制流程。 Agent lies, code not
  • 使用 zod 强校验,避免模型幻觉
  • 完善的 typescript 支持
  • 支持 async await throw catch 等语法
  • 对比其他工具特别轻量,完全基于 mcp 协议
  • 配合 claude code 和 kimi-k2 & deepseek 工作良好

比如用我们再举上面的例子,如果用该工具去处理上面的问题,让 AI 从 1+...+n,同时排除所有位数之和加起来为 5 的倍数的数字,那么完整的写法如下:

import { ClaudeCodeTools, registerWorkflowTool, Prompt, z } from "agent-workflow-mcp-tool";

const server = new McpServer({
  name: "agent-workflow",
  version: "0.0.1",
});

registerWorkflowTool(
  server,
  "workflow",
  {
    title: "workflow",
    description: `workflow control`,
  },
  async function* Workflow() {
    const count = yield* ClaudeCodeTools.AskUserQuestion(
      `please input a number`,
      z.number()
    );

    let sum = 0;
    for (let i = 1; i <= count; i++) {
      if (
        yield* Prompt(
          `计算 "${i}" 的所有位数加起来之和是否为 5 的倍数`,
          z.boolean()
        )
      ) {
      } else {
        sum += i;
      }
    }
    return sum;
  }
);

这时你让 Claude Code 直接执行该 mcp

Ask: use mcp "agent-workflow" tool "workflow" directly

执行结果见下图:

image.png

kimi-k2 真的严格按照代码给定的流程,从 1 + 2 + 直到加到 87,对每一个数字判断其所有数字加起来之和是否为 5 的倍数,调用了 87 次 mcp tool,最终得出了正确的结果。

可以在图中看出,在大模型每次调用 mcp 时,mcp 会给出当前的任务,如果大模型执行成功,大模型需要在下次调用时带上上次任务的执行结果。

每一步都有完善的 zod 类型校验,如果传参不对会给大模型提示,避免大模型的传参幻觉。

基于这样的 workflow,我们也可以把“对每一个数字判断其所有数字加起来之和是否为 5 的倍数”这一步可能会有大模型幻觉产生的步骤,改为使用代码来执行保证。

async function* Workflow() {
  const count = yield* ClaudeCodeTools.AskUserQuestion(
    `please input a number`,
    z.number()
  );

  let sum = 0;
  for (let i = 1; i <= count; i++) {
    if (sumDigits(i) % 5 === 0) {
    } else {
      sum = yield* Prompt(`calculate ${sum} + ${i}`, z.number())
    }
  }
  return sum;
}

更多复杂场景

基于该工具,我们可以实现更复杂的逻辑,比如自动生成 commit 信息并在用户确认后提交代码:

registerWorkflowTool(
    server,
    "auto-commit",
    {
      title: "auto commit",
      description: `auto commit`,
    },
    async function* Workflow() {
      const filesChangeList = yield* Prompt(
        `获取当前变更文件列表`,
        z.array(z.string())
      );

      if (filesChangeList.length === 0) {
        return `没有任何代码更改`;
      }

      const commitMessage = yield* Prompt(
        `根据当前变更内容生成对应的 commit message,格式需满足:
(fix|feat|chore): 单行简洁的提示

多行详细变更内容`,
        z.string()
      );

      const { is_confirm } = yield* ClaudeCodeTools.AskUserQuestion(
        `commit message 为 ${commitMessage},是否确认继续`,
        z.object({
          is_confirm: z.boolean(),
        })
      );

      if (!is_confirm) return `已取消`;

      yield* Prompt(`将当前变更代码提交,commit message 为 ${commitMessage}`);
    }
  );

自动提交确认效果: image.png

还可以基于上面的流程,在 commit 前获取所有变更文件,挨个给每一个文件都使用 AI review 一遍,可以试试看~

使用

npm install agent-workflow-mcp-tool
import { registerWorkflowTool, Prompt, ClaudeCodeTools, z } from 'agent-workflow-mcp-tool';

欢迎使用和反馈~

跳转传参and接收参数

  • route:读取当前路由信息(参数、查询参数、路径等)
  • router:进行路由跳转操作(push、replace、go等)
  1. 跳转
<script setup lang="ts">
import { useRouter } from 'vue-router'

const router = useRouter()

// 跳转到指定路径
router.push('/dashboard')

// 带参数跳转
router.push({ path: '/dashboard', query: { id: '123' } })

// 命名路由跳转
router.push({ name: 'dashboard', params: { id: '123' } })

// 替换当前路由
router.replace('/dashboard')

// 前进/后退
router.go(1)
router.go(-1)
</script>
  1. 读取:
1.直接访问queryd对象

import { useRoute } from 'vue-router'

const route = userRouter()

const id = route.query.id

2.使用computed响应式获取

import { useRoute } from 'vue-router'

import { conputed } from 'vue'

const route = userRouter()

const userId = computed(()=>route.query.id)

3.获取多个参数

import { useRoute } from 'vue-router'

const route = userRouter()

const { id,name,type } = route.query

4.在页面中的生命周期里面接收参数

import { onMounted } from 'vue'

import { useRoute } from 'vue-router'

const route = userRouter()

//生命周期中的挂载后(最常用)

onMounted(()=>{

const id = route.quert.id

console.log("id",id)

})

5.监听路由参数变化

import { watch } from 'vue'

import {useRoute } from 'vue-router'

const route = useRoute()

//监听query参数变化

watch(

()=>route.query.id,

(newId,oldId)=>{

console.log(阐述变化:',oldId,'->',newId)

})

//监听所有query参数变化

watch(()=>route.query,

(newQuery)=>{

console.log('所有参数变化',newQuery)

},

{deep:true}

)

6.在模板中直接使用
<div>ID:{{$route.query.id}}</div>

7.类型安全的方式

import { useRoute } from 'vue-router'

import { conputed } from 'vue'

const route = userRouter()

const query = computed(()=>route.query.id as string | undefined)

if(query.value){
console.log('ID',queryId.value)
}

8.实际应用写在onMounted里面

你知道项目需要什么 node 版本吗?哪个包管理工具的什么版本?

近期接手了一个新项目,clone下来发现

  1. readme 无迹可循,node 版本等信息,只能口口相传,强依赖于上一个开发者
  2. 项目中有 npm, yan, pnpm 相关的配置,却无法知道明确应该使用那个包管理工具

于是开始致力于寻找解决之法

1. 指定 node 版本

1.1. 约束

问题:如果开发者任意使用了某个版本的 node,显然是不符合预期的,所以我们需要添加约束,来尽可能早的暴露错误

目的:开发者可以从项目配置获取到 node 版本信息,以及使用了不符合的 node 版本时warn 或 error

步骤一:最基础约束:package.json → engines

配置方式:

{
  "engines": {
    "node": ">=18 <21"
  }
}

实际效果:

  • npm / pnpm / yarn 会检查

不一样的包管理工具,或同一包管理工具的不同版本,对应行为的说法多种多样,最好自己试一下,下面贴我试下来的结果:

  • npm 10.9.2
  • pnpm 9.5.0
  • yarn 1.22.22

npm: warn

pnpm: warn

yarn: error

步骤二:最基础约束基础上添加engine-strict=true

engine-strict=true

npm: error

pnpm: error

yarn: error (原本也是)

步骤三:脚本约束(最终防线 可选)

const [major] = process.versions.node.split('.').map(Number)

if (major < 18 || major >= 21) {
  console.error(
    `❌ Node.js version ${process.versions.node} is not supported.\n` +
    `Required: >=18 <21`
  )
  process.exit(1)
}
{
  "scripts":{
    "preinstall": "node scripts/check-node.js"
  }
}

1.2. node 版本切换辅助

目的:进入项目实现 node 版本自动切换,或简化手动版本切换

方案一:nvm + .nvmrc (手动)

1️⃣ 在项目根目录新建 .nvmrc

22.16.0

2️⃣ 进入项目执行 nvm use

开发者需要提前安装

  • nvm(macOS / Linux)
  • Windows 需要 nvm-windows

方案二:Volta (自动)

1️⃣ 在项目中 pin node

volta pin node@22.16.0

# 包管理工具一起固定
# volta pin pnpm@9.12.2     # 或 yarn@1.22.19 / npm@10.8.3

生成如下内容,会添加在 package.josn文件中

{
  "volta": {
    "node": "18.19.0"
  }
}

2️⃣ 自动切换 node

不需要手动切换,进入项目后,volta 会在第一次用到 node 时自动切换为目标 node 版本(如果没有目标版本,会自动下载),进入项目 node -v可验证

开发者需要提前安装

  • Volta

注意⚠️:现在 volta 不支持 uninstall node

原因:Volta 把 Node 当成“基础设施”,官方不支持、也不推荐卸载 node

偏方:自己找到文件夹的位置~/.volta/tools/image/node/删掉

2. 指定包管理工具

包管理工具 packageManager (PM)

当看见项目中关于npm,pnpm,yarn包管理工具的配置都存在时,我一脸蒙,不知道应该用哪一个包管理工具,此时明确指定包管理工具才是预期,那么如何指定呢?

2.1. 约束

目的:开发者可以从项目配置获取到可以使用哪个包管理工具,以及使用了不符合的 node 版本时 error

步骤一:package.json -> packageManager(声明 软提醒)

比如指定 pnpm

{
  "packageManager": "pnpm@10.18.3"
}

步骤二: only-allow(强制)

npx only-allow pnpm,npx only-allow npm,npx only-allow yarn

{
  "preinstall": "npx only-allow pnpm"
}

如果有其他脚本,建议把这个放在前面 npx only-allow pnpm && node scripts/check-node.js,此时再用pnpm外的包管理工具可就不行了:

步骤三:脚本约束(最终防线 可选)

const userAgent = process.env.npm_config_user_agent || '';
if (!userAgent.includes('pnpm')) {
  console.error('❌ 请使用 pnpm 安装依赖');
  console.error('💡 运行: corepack enable && pnpm install');
  process.exit(1);
}
{
  "scripts":{
    "preinstall": "npx only-allow pnpm && node scripts/check-node.js"
  }
}

暂时用"preinstall": "node scripts/check-node.js"查看报错:

2.2. 包管理工具切换辅助 ❌

我们没法辅助开发者切换npm/ pnpm /yarn,因为他们本来就不是项目级工具,而是系统级工具,开发者想用哪个用哪个(他尽管用,我们在约束环节已经拦截)

3. 指定包管理工具版本

3.1. 约束

3.1.1. npm 专有约束

1️⃣ package.json#npm

{
  "engines": {
    "node": ">=18 <21",
    "npm": "11.7.0"
  },
}

2️⃣ .npmrc

engine-strict=true

3.1.2. pnpm 专有约束

1️⃣ package.json (声明)

{
  "engines": {
    "node": ">=18 <21",
    "pnpm": "9.1.1"
  },
}

3.1.3. 共享约束:脚本约束

三个PM都可以用的约束,以 pnpm@10.28.2 为例

import * as semver from 'semver';
import { execSync } from 'child_process';

const REQUIRED = '10.28.2';

const current = execSync('pnpm -v').toString().trim();

if (!semver.eq(current, REQUIRED)) {
  console.error(`
❌ pnpm 版本不符合要求

当前版本: ${current}
要求版本: ${REQUIRED}
`);
  process.exit(1);
}

3.2. 包管理工具版本辅助切换

3.2.1. pnpm 自身的版本管理

 {
    "packageManager": "pnpm@10.28.0",
 }

pnpm 触发时,检查 packageManager字段,如果发现不一致会尝试下载并切换到packageManager指定的版本

3.2.2. yarn 依赖 packageManager+corepack

1️⃣ corepack enable

Node.js 版本 ≥ 16.9 <25 自带corepack,没有则先安装corepack

2️⃣ 提供PM信息

{
  "packageManager": "yarn@1.22.20",
}

3️⃣ 自动切换

使用PM时,corepack会读packageManager如果发现版本不一致,触发自动下载

关于corepack可以解决用什么PM(packageManager 包管理工具)?什么PM 版本?

我持怀疑态度,理由如下

corepack 出现的初衷本来就是为了统一PM的版本,而不是统一用户哪一个包版本工具,那都有人说它可以,那尝试一下硬着头皮用。

发现用它来指定PM需要:

  1. 开发者本地存在corepack或Node.js 版本 ≥ 16.9 <25(自带,也不完全自带,如果是 volta下载的,就不会带),且需要corepack enable
  2. 无论 packageManager 配置了什么,都不限制 npm install
  3. 只能限制 corepack下载的包版本工具,但哪个前端开发笔记本不安装几个包管理工具?

发现用它来指PM版本也存在问题:

Corepack 不管理 npm, 配置了npm@10.25.0但是任何版本的npm都会直接执行,不会下载指定版本

结论:❌ 多少有些不可靠,现在能想到的应用场景就只有辅助 yarn 版本切换了

绕死我了!🙂‍↔️🙂‍↔️🙂‍↔️

Nuxt state状态如何管理,3秒手把手帮你

useState。有响应式和支持ssr共享。

useState是支持ssrref替代方案。其中,在后端渲染后(前端水合)会被保留,并通过唯一键在所有组件之间共享。

useState中的数据将序列化为JSON

例子

基本用法

用组件本地的计算器状态。任何用useState('counter')的组件都共享相同的响应式状态。

// app.vue
<script setup lang="ts">
const counter = useState('counter', () => Math.round(Math.random() * 1000))
</script>

<template>
  <div>
    计算器:{{ counter }}
    <button @click="counter++">
      +
    </button>
    <button @click="counter--">
      -
    </button>
  </div>
</template>

初始化状态

异步解析去初始化状态。

// app.vue
<script setup lang="ts">
const websiteConfig = useState('config')

await callOnce(async () => {
  websiteConfig.value = await $fetch('https://my-cms.com/api/website-config')
})
</script>

与Pinia一起用

Pinia 模块创建全局存储并在整个应用中使用它。

// stores/website.ts
export const useWebsiteStore = defineStore('websiteStore', {
  state: () => ({
    name: '',
    description: ''
  }),
  actions: {
    async fetch() {
      const infos = await $fetch('https://api.nuxt.com/modules/pinia')
      
      this.name = infos.name
      this.description = infos.description
    }
  }
})

高级用法

// composables/locale.ts

export const useLocale = () => {
  return useState('locale', () => useDefaultLocale().value)
}

export cosnt useDefautLocale = (fallback = 'en-US') => {
  const locale = ref(fallback)
  return locale
}

export const useLocales = () => {
  const locale = useLocale()
  const locales = ref([
    'en-US',
    'en-GB',
    ...
  ])
  if (!locales.value.includes(locale.value)) {
    locales.value.unshift(locale.value)
  }
  return locales
}

export const useLocaleDate = (date: Ref<Date> | Date, locale = useLocale()) => {
  return computed(() => new Intl.DateTimeFormat(locale.value, {
    dateStyle: 'full'
  }).format(unref(date)))
}
// app.vue
<script setup lang="ts">
const locales = useLocales()
const locale = useLocale()
const date = useLocaleDate(new Date('2026-1-16'))
</script>

<template>
  <div>
    <h1>生日</h1>
    <p>{{ date }}</p>
    <label for="locale-chooser">语言</label>
    <select id="locale-chooser" v-model="locale">
      <option v-for="loc of locales" :key="loc" :value="loc">
        {{ loc }}
      </option>
    </select>
  </div>
</template>

共享状态

自动导入的组合式函数

// composables/states.ts

export const useColor = () => useState<string>('color', () => 'pink')
// app.vue
<script setup lang="ts">
const useColor = () => useState<string>('color', () => 'pink')

const color = useColor() // 与 useState('color')相同
</script>

<template>
  <p>{{ color }}</p>
</template>

用库 - 第三方的

  • Pinia
  • Harlem
  • XState

【前端入门】商品页放大镜效果(仅放大镜随鼠标移动效果)

摘要

依旧是offset系列属性练习。模仿京东放大商品的效果。

一、基本原理

1.利用e.pageX和e.pageY配合offsetTop、offsetLeft获得鼠标在容器内的位置
2.再将获得的位置信息赋值给放大镜的top,left属性
3.最后if条件语句控制放大镜的范围不要超出容器

二、实现过程

1.准备一个大盒子,里面插入商品图片;在准备一个盒子作为放大镜,定位在大盒子里面

<div class = "goods">
    <img src="mobile.jpg" alt = "手机"/>
    <div class = "mask"></div>
</div>

2.为盒子设置样式

<style>
    .goods{
        position:relative;
        width:300px;
        height:400px;
    }
    .goods img{
        width:300px;
        height:400px;
    }
    .mask{
        display:none;
        position:absolute;
        left:0px;
        top:0px;
        width:100px;
        height:100px;
        background-color:yellow;
        opacity:.3;
        cursor:move;
    }
</style>

*cursor:move;这行代码能使鼠标变成十字型

3.绑定事件(鼠标进入显示放大镜;鼠标离开隐藏放大镜;鼠标移动放大镜跟着移动)

<script>
    var goods = document.querySelector(".goods");
    var mask = document.querySelector(".mask");
    goods.addEventListener("mouseenter",function(){
        mask.style.display = "block";
    })
    goods.addEventListener("mouseleave",function(){
        mask.style.display = "none";
    })
    goods.addEventListener("mousemove",function(e){
        var x = e.pageX - this.offsetLeft;
        var y = e.pageY - this.offsetTop;
        var maskX = x - mask.offsetWidth/2;
        var maskY = y - mask.offsetHeight/2;
        if(maskX <= 0){
            maskX = 0;
        }
        else if (maskX >= goods.offsetWidth - mask.offsetWidth){
            maskX = goods.offsetWidth - mask.offsetWidth;
        }
        if(maskY <= 0){
            maskY = 0;
        }
        else if (maskY >= goods.offsetHeight - mask.offsetHeight){
            maskY = goods.offsetHeight - mask.offsetHeight;
        }
        mask.style.left = maskX + 'px';
        mask.style.top = maskY  + 'px';
    })
</script>

a.代码第13、14行的用意是使鼠标始终在放大镜中央,更美观
b.27、28行,在设置top和left属性时一定不要忘记加单位
c.mouseenter与mouseover相比没有冒泡,不会产生冗余事件

三、完整代码示例

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>模拟京东放大镜效果</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        .goods {
            position: relative;
            margin: 100px auto;
            width: 285px;
            height: 292px;
            border: 1px solid #999;
        }

        .goods img {
            width: 283px;
            height: 290px;
        }

        .mask {
            display: none;
            position: absolute;
            left: 0px;
            top: 0px;
            width: 150px;
            height: 150px;
            background-color: rgb(248, 220, 10);
            opacity: .4;
            cursor: move;
        }
    </style>
</head>

<body>
    <div class="goods">
        <img src="../shopping/upload/mobile.jpg" alt="">
        <div class="mask"></div>
    </div>
    <script>
        var goods = document.querySelector(".goods");
        var mask = document.querySelector(".mask");
        goods.addEventListener("mouseover", function () {
            mask.style.display = 'block';
        })
        goods.addEventListener("mouseout", function () {
            mask.style.display = 'none';
        })
        goods.addEventListener("mousemove", function (e) {
            var x = e.pageX - this.offsetLeft;
            var y = e.pageY - this.offsetTop;
            var maskX = x - mask.offsetWidth / 2;
            var maskY = y - mask.offsetHeight / 2;
            if (maskX <= 0) {
                maskX = 0;
            } else if (maskX >= goods.offsetWidth - mask.offsetWidth) {
                maskX = goods.offsetWidth - mask.offsetWidth;
            }
            if (maskY <= 0) {
                maskY = 0;
            } else if (maskY >= goods.offsetHeight - mask.offsetHeight) {
                maskY = goods.offsetHeight - mask.offsetHeight;
            }
            mask.style.left = maskX + 'px';
            mask.style.top = maskY + 'px';

        })
    </script>
</body>

</html>

鸿蒙异步并发 async/await 最佳实践,代码瞬间优雅

Hello,兄弟们,我是 V 哥!

还记得以前写 Android 或者早期 JavaScript 的时候,那个传说中的**“回调地狱”**吗?

// 伪代码演示:让人崩溃的金字塔
login(user, (res1) => {
  getUserInfo(res1.id, (res2) => {
    getOrders(res2.token, (res3) => {
      getDetail(res3.orderId, (res4) => {
        // 终于结束了... 代码已经缩进到屏幕外边了
      })
    })
  })
})

这种代码,维护起来简直是噩梦!但在鸿蒙 ArkTS 的 API 21 环境下,兄弟们千万别再这么写了!ArkTS 是基于 TypeScript 的,它原生支持非常强大的 async/await 语法。

今天 V 哥就带你把这段“金字塔”拍平,用 同步的逻辑写异步的代码,优雅得像喝下午茶一样!


核心心法:把“等待”变成“暂停”

兄弟们,记住 V 哥这两个口诀:

  1. async:加在函数定义前面,表示“这里面有耗时的活儿”。
  2. await:加在耗时的调用前面,表示“等着这儿干完,再去干下一行,但别把界面卡死”。

有了这两个神器,异步代码写出来就像在写小学作文,从上到下,一行一行读,逻辑清晰无比。


实战代码案例

为了让大家直观感受,V 哥写了一个完整的 Demo。咱们模拟三个常见的真实场景:

  1. 串行执行:先登录,再拿用户信息。
  2. 并发执行:同时拉取“广告配置”和“首页推荐”。
  3. 异常处理:优雅地捕获网络错误。

操作步骤: 打开你的 DevEco Studio 6.0,新建一个 ArkTS 页面,把下面的代码完整复制进去,直接运行!

import promptAction from '@ohos.promptAction';

/**
 * V哥的模拟网络请求类
 * 在真实项目中,这里会换成 httpRequest 或者 网络库
 */
class NetworkSimulator {
  // 模拟一个异步耗时操作,返回 Promise
  static request(apiName: string, data: string, delay: number): Promise<string> {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        // 模拟 20% 的概率失败
        if (Math.random() < 0.2) {
          reject(new Error(`${apiName} 请求失败,网络不给力!`));
        } else {
          resolve(`${apiName} 返回的数据: ${data}`);
        }
      }, delay);
    });
  }
}

@Entry
@Component
struct AsyncAwaitDemo {
  @State resultLog: string = 'V哥准备好输出日志了...';
  @State isLoading: boolean = false;

  build() {
    Column() {
      Text('鸿蒙 async/await 实战实验室')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 30, bottom: 20 })

      // 场景一:串行执行
      Button('场景1:串行执行 (登录 -> 获取信息)')
        .width('90%')
        .margin({ bottom: 15 })
        .onClick(() => {
          this.testSequential();
        })

      // 场景二:并发执行
      Button('场景2:并发执行 (同时拉取配置和广告)')
        .width('90%')
        .margin({ bottom: 15 })
        .onClick(() => {
          this.testParallel();
        })

      // 场景三:异常捕获
      Button('场景3:异常捕获 (模拟失败重试)')
        .width('90%')
        .margin({ bottom: 15 })
        .onClick(() => {
          this.testErrorHandling();
        })

      // 日志显示区域
      Column() {
        Text(this.resultLog)
          .fontSize(14)
          .fontColor('#333333')
          .width('100%')
      }
      .width('90%')
      .height('40%')
      .padding(15)
      .backgroundColor('#F1F3F5')
      .borderRadius(10)
      .margin({ top: 20 })

      if (this.isLoading) {
        LoadingProgress()
          .width(30)
          .height(30)
          .margin({ top: 20 })
          .color(Color.Blue)
      }

    }
    .width('100%')
    .height('100%')
    .padding({ left: 20, right: 20 })
  }

  /**
   * V哥解析:场景1 - 串行执行
   * 特点:一步接一步,下一步依赖上一步的结果。
   * 代码逻辑:完全是线性的,像同步代码一样易读!
   */
  async testSequential() {
    this.isLoading = true;
    this.resultLog = '1. 开始登录...\n';

    try {
      // V哥重点:await 会暂停函数执行,直到 Promise resolve
      // 这里模拟先登录,耗时 1000ms
      let loginRes = await NetworkSimulator.request('LoginAPI', 'Token123', 1000);
      this.resultLog += `   ${loginRes}\n`;

      this.resultLog += '2. 正在获取用户信息...\n';
      // 依赖上面的 Token,继续 await
      let userRes = await NetworkSimulator.request('GetUserInfo', 'V哥的大名', 800);
      this.resultLog += `   ${userRes}\n`;

      this.resultLog += '✅ 全部完成!(串行总耗时约 1.8s)';
      
      promptAction.showToast({ message: '串行执行完成' });

    } catch (error) {
      this.resultLog += `❌ 出错了: ${error.message}`;
    } finally {
      this.isLoading = false;
    }
  }

  /**
   * V哥解析:场景2 - 并发执行
   * 特点:两个请求互不依赖,同时发出,谁先回来谁先结束。
   * 优势:速度最快!总耗时 = 两个请求中最慢的那个,而不是两者之和。
   */
  async testParallel() {
    this.isLoading = true;
    this.resultLog = '1. 同时启动多个任务...\n';

    // 记录开始时间
    const startTime = Date.now();

    try {
      // V哥重点:Promise.all()
      // 把所有要并发的 Promise 放进数组里
      // await 会等数组里所有的 Promise 都 resolve 才继续
      let results = await Promise.all([
        NetworkSimulator.request('GetConfig', '系统配置', 1500), // 假设这个慢
        NetworkSimulator.request('GetBanner', '广告图片', 1000)  // 假设这个快
      ]);

      // results 是一个数组,顺序和你传入的顺序一致,不管谁先回来
      this.resultLog += `   ${results[0]}\n`; // 第一个结果
      this.resultLog += `   ${results[1]}\n`; // 第二个结果

      const duration = Date.now() - startTime;
      this.resultLog += `✅ 全部完成!(并发总耗时约 ${duration}ms,比串行快!)`;
      
      promptAction.showToast({ message: '并发执行完成' });

    } catch (error) {
      this.resultLog += `❌ 出错了: ${error.message}`;
    } finally {
      this.isLoading = false;
    }
  }

  /**
   * V哥解析:场景3 - 异常处理
   * 特点:async/await 下,我们用 try...catch...finally 代替 .then().catch()
   * 这比传统的 Promise 链式调用要直观得多,像处理 Java 异常一样舒服。
   */
  async testErrorHandling() {
    this.isLoading = true;
    this.resultLog = '尝试发送请求 (模拟20%失败率)...\n';

    try {
      // 这里的请求可能会抛出 Error
      let data = await NetworkSimulator.request('RiskyAPI', '试试运气', 1000);
      this.resultLog += `   成功: ${data}`;
      promptAction.showToast({ message: '请求成功' });

    } catch (error) {
      // V哥重点:一旦任何一步 await 报错,直接跳进 catch
      this.resultLog += `   捕获到异常: ${error.message}\n`;
      this.resultLog += `   这里可以进行重试逻辑...`;
      
      promptAction.showToast({ message: '请求被拦截' });

    } finally {
      // V哥重点:finally 无论成功失败都会执行
      // 适合用来关闭 Loading 弹窗
      this.isLoading = false;
    }
  }
}

运行结果:


V 哥的代码深度解析

兄弟们,代码能跑了,咱们得懂原理,不然面试的时候要挂!

1. 为什么 async/await 不会卡死界面?

这就是并发编程的魔力。 当你写 let res = await someRequest() 的时候,ArkTS 的运行时会把当前任务的挂起,把主线程的控制权交还给 UI 系统。 这就好比你去排队买奶茶,你叫服务员做奶茶(发起请求),你站在旁边等(await),但**店里的其他人(UI线程)**依然可以继续进店买东西。只有当你的奶茶好了(Promise resolve),你才拿着奶茶走人(代码继续往下走)。

2. Promise.all 是性能优化的利器

在场景 2 中,V 哥演示了 Promise.all。 如果你的首页有 5 个接口,互不依赖,你千万别写 5 行 await:

// ❌ 错误写法:慢得要死
let a = await req1(); // 等1秒
let b = await req2(); // 再等1秒
// ... 总耗时 5秒
// ✅ V 哥正确写法:飞快
let results = await Promise.all([req1(), req2(), req3(), req4(), req5()]);
// 总耗时 = 最慢的那个接口 (假设是 1.2秒)

这可是实打实的性能提升,用户打开 App 的速度直接肉眼可见变快!

3. 不要忘记了 try-catch

以前写 Promise 链,如果不加 .catch(),报错了可能就像石沉大海,静默失败。 用了 async/await一定要 包裹在 try...catch 里。这是对自己代码负责,也是对用户负责。


总结

来来来,V 哥稍微小结一下:

  1. 逻辑复杂?async/await 拍平金字塔。
  2. 请求多且慢?Promise.all 并行加速。
  3. 怕出错?try/catch 稳稳兜底。

在 DevEco Studio 6.0 里,这套组合拳用熟练了,你的代码质量和开发效率绝对能甩开同行一条街。

我是 V 哥,拒绝回调地狱,从今天开始!咱们下期见!👋

你的手势冲突解决了吗?鸿蒙事件拦截机制全解析

哈喽,兄弟们,我是 V 哥! 在鸿蒙开发中,尤其是做复杂的交互页面(比如列表里套按钮横滑菜单地图缩放)时,手势事件就像是一群调皮的孩子,谁都想抢着接盘。如果你不管好他们,App的体验会差强人意。

关于鸿蒙API 21 的事件拦截机制。这三招,专治各种“乱跳”、“误触”和“滑动失效”。代码我都给你写好了,直接复制就能治好你的 App!


痛点一:点击冒泡 —— “我点的按钮,你关列表什么事?”

📜 案发现场

最常见的场景:一个 ListItem 本身是可以点击跳转详情的,但里面有一个“删除”按钮。 用户想点删除,结果手指稍微偏了一点点,或者系统判定失误,不仅删除了数据,还顺手跳到了详情页。用户体验极其糟糕。

🔍 原理剖析

这是典型的事件冒泡。触摸事件从子组件(按钮)传递到父组件(列表项)。子组件处理完了,如果没说“别传了”,父组件就会觉得:“哦?有人点了我的地盘?那我也响应一下吧。”

✅ V 哥的一招制敌:hitTestBehavior

我们要做的就是:给子组件(按钮)设个“路障”,告诉父组件:这事我办了,你别插手!


痛点二:滑动打架 —— “我想横滑,你非要竖着滚?”

📜 案发现场

你在做一个音乐播放器,进度条支持横向拖动。但是,这个播放器是放在一个 Scroll(垂直滚动)容器里的。 当你想拖动进度条时,手指稍微带点垂直角度,页面就开始上下滚动,进度条根本拖不动。

🔍 原理剖析

父容器的 VerticalScroll(垂直滚动手势)和子组件的 PanGesture(拖动手势)发生了竞争。系统不知道你是想切歌还是想看歌词。

✅ V 哥的一招制敌:PanGesture & ParallelGesture

我们需要精细化控制手势的方向并发模式


代码案例

我们打开 DevEco Studio 6.0,新建一个页面 GestureDemo.ets。这段代码包含了上面两个问题的完整解决方案,跑一遍你就全懂了。

import promptAction from '@ohos.promptAction';

@Entry
@Component
struct GestureDemo {
  @State deleteLog: string = '操作日志:等待操作...';

  build() {
    Column() {
      Text('V哥的手势冲突诊疗室')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 30, bottom: 20 })

      // ==========================================
      // 场景一:点击冲突(冒泡问题)
      // ==========================================
      Text('场景1:列表项点击 vs 按钮点击')
        .fontSize(18)
        .margin({ bottom: 10 })
        .fontColor('#666')

      // 模拟一个列表项
      Row() {
        Text('V哥的鸿蒙实战教程.mp4')
          .layoutWeight(1)
        
        Button('删除')
          .fontSize(14)
          .backgroundColor(Color.Red)
          .padding({ left: 12, right: 12 })
          // --- V哥的关键神技 ---
          // HitTestMode.Block 表示:我(按钮)是挡箭牌。
          // 只要点击落在按钮区域,就由我处理,绝不传给父组件 Row。
          // 这样父组件的 onClick 就不会被误触了!
          .hitTestBehavior(HitTestMode.Block)
          .onClick(() => {
            this.deleteLog = '🔥 操作:点击了【删除】按钮(父组件被拦截)';
            promptAction.showToast({ message: '删除成功!' });
          })
      }
      .width('100%')
      .padding(15)
      .backgroundColor('#F1F3F5')
      .borderRadius(8)
      .margin({ bottom: 20 })
      .onClick(() => {
        // 点击 Row 的空白处会触发这里,但点按钮不会
        this.deleteLog = '📖 操作:点击了【列表项】,应该跳转详情';
      })

      // ==========================================
      // 场景二:滑动冲突(纵横问题)
      // ==========================================
      Text('场景2:竖向滚动 vs 横向拖拽')
        .fontSize(18)
        .margin({ bottom: 10 })
        .fontColor('#666')

      // 外层:竖向滚动容器
      Scroll() {
        Column() {
          Text('这是顶部内容')
            .height(100)
            .width('100%')
            .backgroundColor(Color.Pink)

          // 这是一个专门用于横向拖拽的区域
          Row() {
            Text('拖动我 -> ')
              .fontSize(16)
            Text(this.value.toString())
              .fontSize(16)
          }
          .width('90%')
          .height(100)
          .backgroundColor(Color.Orange)
          .borderRadius(8)
          .justifyContent(FlexAlign.Center)
          .margin({ top: 20 })
          // --- V哥的关键神技 ---
          // 1. 定义一个横向拖动手势
          .gesture(
            PanGesture({ direction: PanDirection.Horizontal })
              .onActionStart(() => {
                this.deleteLog = '🤚 操作:开始【横向】拖拽';
              })
              .onActionUpdate((event: GestureEvent) => {
                // V哥演示:简单累加一下偏移量
                this.value += event.offsetX;
              })
          )

          Text('这是底部内容,多撑开点高度')
            .height(400)
            .width('100%')
            .backgroundColor(Color.Grey)
        }
        .width('100%')
      }
      .width('100%')
      .height(300)
      .scrollable(ScrollDirection.Vertical) // 申明竖向滚动
      .border({ width: 2, color: Color.Blue })

      // 日志显示
      Text(this.deleteLog)
        .fontSize(14)
        .fontColor('#333')
        .margin({ top: 20 })
        .padding(10)
        .width('100%')
        .borderRadius(5)
        .backgroundColor('#E0E0E0')

    }
    .width('100%')
    .height('100%')
    .padding({ left: 20, right: 20 })
  }

  // 用于存储滑块值的变量
  @State value: number = 0;
}

复盘一下:手势机制的三个“挡箭牌”

代码跑通了,咱们得把 API 21 里的这几个参数吃透,以后遇到变种 Bug 也能一招制敌。

1. hitTestBehavior 的四个境界

这是最常用的属性,修饰在组件上。

  • HitTestMode.Default(默认)

    • 特点:该谁是谁。如果组件本身是 Button 这类可点击的,它就拦截;如果是 Text 这种,它就放行给父组件。
    • V 哥吐槽:有时候系统误判,导致布局透明的 Row 挡住了下层按钮,这时候你就得改它。
  • HitTestMode.None(透明人)

    • 特点:我不拦截。点击我这个区域,就好像我不存在一样,事件直接穿透我,传给我的孩子或者兄弟。
    • 场景:你做了一个复杂的背景布局,但不想它挡住背后的按钮。
  • HitTestMode.Block(拦路虎):🌟 V 哥推荐

    • 特点:我全收了。不管我下面是什么,只要点到我,我就处理,绝不往外传。
    • 场景:这就是咱们代码里解决“列表里套按钮”的神器。给按钮加上它,父组件再也不会误触跳转。
  • HitTestMode.Transparent(传声筒)

    • 特点:我拦截到事件后,处理完,还要传给父组件。
    • 场景:很少用,除非你想实现“点子组件,父子一起动”的效果(通常不推荐,容易乱)。

2. 手势的优先级

如果两个手势都想响应,听谁的?

  • 系统默认TapGesture (点击) > LongPressGesture (长按) > PanGesture (拖动) > PinchGesture (捏合)。
  • 手动干预:如果你想强行让某个手势优先,可以用 priorityGesture 包裹手势。
    .gesture(
      // 即使父组件想滚动,子组件的横向拖动优先级更高
      priorityGesture(PanGesture({ direction: PanDirection.Horizontal }))
    )

3. 并发手势

如果两个手势可以同时发生(比如一边缩放一边旋转),用 GestureGroup 配合 GestureMode.Parallel。 不过,对于大多数“纵横冲突”,鸿蒙系统 API 21 已经能很智能地通过 PanGesturedirection 属性自动区分方向了。如果你发现它分不清,通常是布局重叠或者触摸区域设置不合理导致的。


小结一下

下次再做列表、相册、播放器的时候,把这三招拿出来,产品经理看你的眼神绝对不一样!

  1. 怕误触(点击冲突):给按钮加 hitTestBehavior(HitTestMode.Block)
  2. 怕抢滑(滑动冲突):给子组件绑定明确方向的 PanGesture,必要时加 priorityGesture
  3. 怕透传:给遮挡层加 hitTestBehavior(HitTestMode.None)

我是V哥,咱们下期技术复盘见!

Lodash 源码解读与原理分析 - Lodash IIFE 与兼容性处理详解

一、IIFE 结构详解

Lodash 整体代码被包裹在一个完整的立即调用函数表达式(IIFE)中。

;(function() {
  // 核心实现...
}.call(this));

1. IIFE 的关键技术点

a. 分号前缀:防御性编程的典范

;(function() { /* 实现 */ }.call(this));

设计背景:早期 JavaScript 开发中,很多开发者会省略语句末尾的分号(如 var a = 1 后无分号),若 Lodash 代码前的脚本未正确结束,IIFE 会与前序代码拼接导致语法错误(如 var a = 1(function(){})())。

核心作用:分号前缀强制终止前序语句,确保 IIFE 作为独立语句执行,是 JavaScript 库开发中最基础的防御性编程技巧。

b. 上下文绑定:统一全局对象引用

(function() { /* 实现 */ }.call(this));

设计背景:不同环境中 this 的指向不同 —— 浏览器全局作用域中 this 指向 window,Node.js 全局作用域中 this 指向 global,Web Worker 中指向 self

核心作用:通过 call(this) 将 IIFE 内部的 this 绑定到运行环境的全局对象,确保后续环境检测逻辑能统一获取全局上下文,避免硬编码 window/global 导致的环境适配问题。

c. 作用域隔离:避免全局污染

IIFE 会创建独立的函数作用域,Lodash 内部的所有变量(如 baseCreaterootfreeGlobal)均不会泄漏到全局作用域,仅通过最后导出的 _ 变量对外暴露 API。

对比示例

// 无 IIFE:变量泄漏到全局
var VERSION = '4.17.21'; // 全局变量 VERSION 被污染
function baseCreate() {} // 全局函数 baseCreate

// 有 IIFE:变量隔离在内部作用域
;(function() {
  var VERSION = '4.17.21'; // 仅在 IIFE 内部可访问
  function baseCreate() {}
}.call(this));

二、环境检测机制

Lodash 的兼容性核心是 “先检测、后适配”—— 通过精准的环境检测,识别运行环境的特性和限制,再选择对应的实现方案,而非暴力降级。

1. 全局对象检测

全局对象是跨环境适配的核心,Lodash 设计了多层级的全局对象检测逻辑,覆盖所有主流 JavaScript 运行环境:

/** Detect free variable `global` from Node.js. */
var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;

/** Detect free variable `self`. */
var freeSelf = typeof self == 'object' && self && self.Object === Object && self;

/** Used as a reference to the global object. */
var root = freeGlobal || freeSelf || Function('return this')();

逐行解析:

  1. Node.js 环境检测

    var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
    
    • typeof global == 'object':确保 global 存在且为对象类型(排除 global 被覆盖为其他类型的情况);
    • global:非空校验(避免 globalnull/undefined);
    • global.Object === Object:核心校验 —— 确保 global 是真正的全局对象,而非被篡改的伪全局对象(如 var global = { Object: {} });
    • 最终返回 globalfalse
  2. 浏览器 / Web Worker 环境检测

    var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
    
    • self 是浏览器 / Web Worker 的标准全局对象,比 window 更通用(Web Worker 中无 window,但有 self);
    • 校验逻辑与 freeGlobal 一致,确保获取真实的全局对象。
  3. 兜底方案

    var root = freeGlobal || freeSelf || Function('return this')();
    
    • Function('return this')():通过动态创建函数并执行,在严格模式 / 受限环境中也能获取全局对象(ES5 规范中,无上下文调用函数时 this 指向全局对象);
    • 优先级:Node.js(freeGlobal)> 浏览器 / Web Worker(freeSelf)> 兜底方案。

环境测试案例:

运行环境 root 指向 检测逻辑
Node.js v18 global freeGlobal 为 true,直接返回
Chrome 120 self freeSelf 为 true,直接返回
Web Worker self freeSelf 为 true,直接返回
IE8(无 self window freeGlobal/freeSelf 为 false,执行 Function('return this')() 返回 window
严格模式下的浏览器 window 兜底方案不受严格模式影响,仍返回全局对象

2. 模块系统检测

Lodash 支持 AMD/CommonJS/全局变量三种导出方式,核心依赖精准的模块系统检测逻辑:

/** Detect free variable `exports`. */
var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;

/** Detect free variable `module`. */
var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module;

/** Detect the popular CommonJS extension `module.exports`. */
var moduleExports = freeModule && freeModule.exports === freeExports;

逐行解析:

  1. CommonJS exports 检测

    var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;
    
    • typeof exports == 'object':检测 exports 是否为对象(CommonJS 环境的核心特征);
    • !exports.nodeType:关键校验 —— 排除 DOM 节点(如 <div id="exports"> 会导致 window.exports 指向该节点);
    • 确保 exports 是 CommonJS 模块系统的导出对象,而非同名 DOM 节点。
  2. CommonJS module 检测

    var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module;
    
    • 依赖 freeExports 为真:仅在检测到 exports 后才检测 module
    • 同样通过 !module.nodeType 排除 DOM 节点污染。
  3. module.exports 一致性检测

    var moduleExports = freeModule && freeModule.exports === freeExports;
    
    • 验证 module.exportsexports 指向同一对象(CommonJS 规范要求);
    • 避免 module.exports 被手动修改导致导出异常。

设计思路:

Lodash 优先检测模块系统,再考虑全局变量,符合 “模块化优先、全局兼容兜底” 的现代开发理念;同时通过 nodeType 校验,解决了浏览器中 DOM 节点与模块变量同名的经典兼容问题。

3. API 支持检测

Lodash 会检测环境中原生 API 的支持情况,优先使用高性能的原生实现,无支持时则提供自定义降级方案:

/** Detect free variable `process` from Node.js. */
var freeProcess = moduleExports && freeGlobal.process;

/** Used to access faster Node.js helpers. */
var nodeUtil = (function() {
  try {
    // Use `util.types` for Node.js 10+.
    var types = freeModule && freeModule.require && freeModule.require('util').types;

    if (types) {
      return types;
    }

    // Legacy `process.binding('util')` for Node.js < 10.
    return freeProcess && freeProcess.binding && freeProcess.binding('util');
  } catch (e) {}
}());

/* Node.js helper references. */
var nodeIsArrayBuffer = nodeUtil && nodeUtil.isArrayBuffer,
    nodeIsDate = nodeUtil && nodeUtil.isDate,
    nodeIsMap = nodeUtil && nodeUtil.isMap,
    nodeIsRegExp = nodeUtil && nodeUtil.isRegExp,
    nodeIsSet = nodeUtil && nodeUtil.isSet,
    nodeIsTypedArray = nodeUtil && nodeUtil.isTypedArray;

逐行解析:

  1. Node.js process 检测

    var freeProcess = moduleExports && freeGlobal.process;
    
    • 仅在 CommonJS 环境中检测 process 对象(浏览器中无 process);
    • 依赖 moduleExports 为真,避免浏览器中伪造 process 导致误判。
  2. Node.js 工具模块适配

    var nodeUtil = (function() {
      try {
        // Node.js 10+ 推荐使用 util.types
        var types = freeModule && freeModule.require && freeModule.require('util').types;
        if (types) return types;
        // Node.js < 10 降级使用 process.binding('util')
        return freeProcess && freeProcess.binding && freeProcess.binding('util');
      } catch (e) {}
    }());
    
    • try-catch 包裹:避免 require('util')process.binding('util') 抛出异常(如某些受限 Node.js 环境禁用 process.binding);
    • 版本适配:区分 Node.js 10+ 和低版本,选择对应的类型检测 API;
    • 优雅降级:获取失败时返回 undefined,后续使用自定义类型检测逻辑。
  3. Node.js 类型检测 API 缓存

    var nodeIsArrayBuffer = nodeUtil && nodeUtil.isArrayBuffer;
    
    • 缓存原生 API 引用,避免多次属性查找,提升性能;
    • 短路求值:若 nodeUtilundefined,直接返回 undefined,后续自动使用自定义实现。

性能对比:

Node.js 原生 util.types.isArrayBuffer 比 Lodash 自定义的 isArrayBuffer 快约 30%,Lodash 通过 “原生优先、降级兜底” 的策略,在兼容低版本的同时最大化性能。

三、兼容性处理核心实现

1. baseCreate:原型创建的兼容实现

baseCreate 是 Lodash 原型继承体系的基石,实现了跨环境的 Object.create 兼容,是所有包装器(LodashWrapper/LazyWrapper)原型创建的核心工具:

var baseCreate = (function() {
  function object() {}
  return function(proto) {
    if (!isObject(proto)) {
      return {};
    }
    if (objectCreate) {
      return objectCreate(proto);
    }
    object.prototype = proto;
    var result = new object;
    object.prototype = undefined;
    return result;
  };
}());

设计背景:

Object.create 是 ES5 新增的 API,IE8 及以下版本不支持,而 Lodash 需要兼容这些低版本环境;同时,Object.create 本身也有边界情况(如 proto 非对象时返回空对象),需要统一处理。

逐行解析:

  1. 闭包缓存空构造函数

    var baseCreate = (function() {
      function object() {} // 空构造函数,用于模拟 Object.create
      return function(proto) { /* 实现 */ };
    }());
    
    • 通过 IIFE 创建闭包,缓存 object 构造函数,避免每次调用 baseCreate 时重新创建,提升性能;
    • object 构造函数无任何逻辑,确保创建的实例纯净无多余属性。
  2. 参数类型校验

    if (!isObject(proto)) {
      return {};
    }
    
    • 调用 isObject 检测 proto 是否为对象 / 函数(排除 null、基本类型);
    • 非对象时返回空对象,与 Object.create 的行为一致(Object.create(123) 会报错,Lodash 此处做了更友好的降级)。
  3. 原生 API 优先

    if (objectCreate) {
      return objectCreate(proto);
    }
    
    • objectCreate 是 Lodash 提前检测的 Object.create 引用;
    • 优先使用原生 Object.create,保证性能和标准行为。
  4. 低版本环境降级

    object.prototype = proto;
    var result = new object;
    object.prototype = undefined;
    return result;
    
    • 步骤 1:将空构造函数的原型设置为传入的 proto
    • 步骤 2:创建构造函数实例,该实例的 __proto__ 指向 proto
    • 步骤 3:重置构造函数原型为 undefined,避免后续调用污染;
    • 步骤 4:返回实例,实现与 Object.create(proto) 相同的原型继承效果。

兼容效果验证:

环境 baseCreate({ a: 1 }) 结果 原型链
Chrome 120 {} obj.__proto__ → { a: 1 }
IE8 {} obj.__proto__ → { a: 1 }
Node.js v0.10 {} obj.__proto__ → { a: 1 }
传入非对象(如 123 {} obj.__proto__ → Object.prototype

2. 特性检测与降级处理

Lodash 对数组、对象、函数等核心 API 都做了特性检测和降级处理,确保不同环境下行为一致。

a. 数组方法的兼容实现

/** Used for built-in method references. */
var arrayProto = Array.prototype;

/** Built-in method references without a dependency on `root`. */
var push = arrayProto.push,
    slice = arrayProto.slice;

/**
 * A specialized version of `_.forEach` for arrays without support for
 * iteratee shorthands.
 *
 * @private
 * @param {Array} [array] The array to iterate over.
 * @param {Function} iteratee The function invoked per iteration.
 * @returns {Array} Returns `array`.
 */
function arrayEach(array, iteratee) {
  var index = -1,
      length = array == null ? 0 : array.length;

  while (++index < length) {
    if (iteratee(array[index], index, array) === false) {
      break;
    }
  }
  return array;
}

核心设计思路:

  1. 原生方法缓存

    • var push = arrayProto.push:缓存数组原生方法,避免每次调用时通过 Array.prototype 查找,提升性能;
    • 不依赖 root(全局对象),避免全局对象被篡改导致的异常。
  2. 边界值处理

    • length = array == null ? 0 : array.length:处理 arraynull/undefined 的情况,避免 Cannot read property 'length' of null 错误;
    • 与原生 Array.prototype.forEach 行为一致(原生 forEach 调用 null/undefined 会报错,Lodash 做了容错)。
  3. 提前终止机制

    • if (iteratee(...) === false) break:支持返回 false 终止遍历,弥补原生 forEach 无法中断的缺陷;
    • 保持与 Lodash 其他遍历方法的行为一致性。

b. 对象方法的兼容实现

/** Used for built-in method references. */
var objectProto = Object.prototype;

/** Used to resolve the decompiled source of functions. */
var fnToString = Function.prototype.toString;

/** Used to detect host constructors (Safari). */
var reIsHostCtor = /^[object .+?Constructor]$/;

/** Used to detect unsigned integer values. */
var reIsUint = /^(?:0|[1-9]\d*)$/;

/**
 * Checks if `value` is a host object in IE < 9.
 *
 * @private
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a host object, else `false`.
 */
function isHostObject(value) {
  // IE < 9 presents many host objects as `Object` objects that can coerce to
  // strings despite having improperly defined `toString` methods.
  var result = false;
  if (value != null && typeof value.toString != 'function') {
    try {
      result = !!(value + '');
    } catch (e) {}
  }
  return result;
}

设计背景与解析:

  1. IE < 9 宿主对象兼容

    • IE < 9 中,DOM 节点、XMLHttpRequest 等宿主对象会被识别为 Object 类型,但没有标准的 toString 方法;
    • value + '':尝试将宿主对象转换为字符串,判断是否为宿主对象;
    • try-catch 包裹:避免转换失败抛出异常(如某些宿主对象不支持字符串拼接)。
  2. 正则检测宿主构造函数

    • reIsHostCtor = /^[object .+?Constructor]$/:检测 Safari 中宿主构造函数(如 WindowConstructorDocumentConstructor);
    • 解决 Safari 中宿主对象类型检测不准确的问题。

3. 模块导出的兼容性

Lodash 支持 AMD/CommonJS/ 全局变量三种导出方式,确保在不同模块系统中都能正确引入:

// Export lodash.
var _ = runInContext();

// Some AMD build optimizers, like r.js, check for condition patterns like:
if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) {
  // Expose Lodash on the global object to prevent errors when Lodash is
  // loaded by a script tag in the presence of an AMD loader.
  // See http://requirejs.org/docs/errors.html#mismatch for more details.
  // Use `_.noConflict` to remove Lodash from the global object.
  root._ = _;

  // Define as an anonymous module so, through path mapping, it can be
  // referenced as the "underscore" module.
  define(function() {
    return _;
  });
}
// Check for `exports` after `define` in case a build optimizer adds it.
else if (freeModule) {
  // Export for Node.js.
  (freeModule.exports = _)._ = _;
  // Export for CommonJS support.
  freeExports._ = _;
}
else {
  // Export to the global object.
  root._ = _;
}

逐行解析与设计思路:

  1. AMD 模块导出

    if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) {
      root._ = _; // 暴露到全局,避免脚本标签加载时的冲突
      define(function() { return _; }); // 定义匿名 AMD 模块
    }
    
    • 匿名模块:支持通过路径映射(如 RequireJS)将 Lodash 映射为 underscore,兼容 Underscore.js 的用户;
    • 全局暴露:解决 AMD 加载器存在时,脚本标签引入 Lodash 导致的 “模块不匹配” 错误(参考 RequireJS 官方文档);
    • _.noConflict():预留全局变量冲突解决方法,用户可调用该方法恢复原有 _ 变量。
  2. CommonJS 模块导出

    javascript

    运行

    else if (freeModule) {
      (freeModule.exports = _)._ = _; // 主导出为 _ 实例
      freeExports._ = _; // 兼容 exports._ 方式引入
    }
    
    • (freeModule.exports = _)._ = _:链式赋值,既将 module.exports 设为 _,又为其添加 _ 属性(require('lodash')._ === require('lodash'));
    • 兼容 const _ = require('lodash')const lodash = require('lodash')._ 两种引入方式。
  3. 全局变量导出

    javascript

    运行

    else {
      root._ = _; // 挂载到全局对象
    }
    
    • 兜底方案,覆盖无模块系统的环境(如直接通过 <script> 标签引入);
    • 使用 root 而非硬编码 window,确保跨环境兼容(如 Web Worker 中 root 指向 self)。

导出方式测试案例:

引入方式 代码示例 能否正常使用
AMD(RequireJS) require(['lodash'], function(_) { _.map([1,2], n=>n*2) })
CommonJS(Node.js) const _ = require('lodash'); _.sum([1,2,3])
ES Module(现代 Node.js) import _ from 'lodash'; _.filter([1,2], n=>n>1) ✅(Node.js 自动兼容)
全局变量(浏览器) <script src="lodash.js"></script>; _.each([1,2], console.log)

四、兼容性处理的技术要点

1. 全局对象的获取策略

Lodash 的全局对象获取策略是跨环境库开发的典范,兼顾兼容性、安全性和性能:

var root = freeGlobal || freeSelf || Function('return this')();

核心优势:

  1. 优先级合理

    • 优先 Node.js(freeGlobal)→ 其次浏览器 / Web Worker(freeSelf)→ 最后兜底方案;
    • 符合 “常用环境优先” 的原则,减少兜底方案的调用次数。
  2. 安全性高

    • 通过 global.Object === Object 等校验,确保获取的是真实全局对象;
    • 避免全局对象被篡改导致的异常(如 window = { Object: {} })。
  3. 兼容性无死角

    • 兜底方案 Function('return this')() 不受严格模式影响(严格模式下全局函数的 this 仍指向全局对象);
    • 覆盖所有 JavaScript 运行环境,包括冷门的 Rhino、Nashorn 等。

反例对比:

// 糟糕的全局对象获取方式:硬编码 window,不兼容 Node.js/Web Worker
var root = window;

// 糟糕的全局对象获取方式:无校验,易被篡改
var root = global || self || window;

2. 特性检测的实现模式

Lodash 采用三种特性检测模式,覆盖所有原生 API 的兼容场景:

a. 直接检测:适用于全局 API

var objectCreate = Object.create;
  • 适用场景:检测 Object.createSymbol 等全局对象的属性;
  • 优势:简单高效,无性能损耗;
  • 注意:需提前检测 Object 是否存在(极端环境下可能缺失)。

b. 类型检查检测:适用于构造函数 / 方法

var symIterator = typeof Symbol == 'function' && Symbol.iterator;
  • 适用场景:检测构造函数(如 Symbol)或其属性(如 Symbol.iterator);
  • 优势:避免直接访问不存在的属性导致的错误;
  • 短路求值typeof Symbol == 'function' 为 false 时,不会执行后续的 Symbol.iterator

c. try-catch 检测:适用于可能抛出异常的 API

var nodeUtil = (function() {
  try {
    var types = freeModule && freeModule.require && freeModule.require('util').types;
    if (types) return types;
    return freeProcess && freeProcess.binding && freeProcess.binding('util');
  } catch (e) {}
}());
  • 适用场景:检测 Node.js 特定 API(如 process.binding)、DOM 方法等可能抛出异常的 API;
  • 优势:优雅处理 API 不存在 / 权限不足的情况,避免程序崩溃;
  • 注意:try-catch 有轻微性能损耗,仅用于必要场景。

3. 性能优化与兼容性的平衡

Lodash 在保证兼容性的同时,通过多种优化手段提升性能,避免 “兼容即慢” 的问题:

a. 缓存常用引用

/** Used for built-in method references. */
var arrayProto = Array.prototype,
    objectProto = Object.prototype;

/** Built-in method references without a dependency on `root`. */
var push = arrayProto.push,
    slice = arrayProto.slice,
    toString = objectProto.toString;
  • 优化原理

    1. 减少原型链查找:每次调用 push 时,无需通过 Array.prototype.push 查找,直接使用缓存的引用;
    2. 降低依赖:不依赖 root 全局对象,避免全局对象被篡改导致的性能损耗;
    3. 提升压缩率:短变量名(如 push)比长路径(Array.prototype.push)更易被压缩工具优化。

b. 条件分支优化

function baseEach(collection, iteratee) {
  if (collection == null) {
    return collection;
  }
  if (!isArrayLike(collection)) {
    return baseForOwn(collection, iteratee);
  }
  var length = collection.length,
      index = -1;

  while (++index < length) {
    if (iteratee(collection[index], index, collection) === false) {
      break;
    }
  }
  return collection;
}
  • 优化原理

    1. 快速路径:优先处理 collection == null 的情况,直接返回,避免不必要的计算;
    2. 类型分支:根据集合类型(数组 / 类数组 vs 对象)选择最优遍历方式(while 循环 vs for-in 循环);
    3. 提前返回:遍历过程中支持返回 false 终止循环,减少无效迭代;
    4. 减少属性访问:缓存 collection.length,避免每次循环都访问属性。

性能数据:

Lodash 的 baseEach 比原生 forEach 快约 15%(数组场景),比 for-in 循环快约 40%(对象场景),核心原因就是条件分支优化和缓存策略。

五、总结

Lodash 的 IIFE 结构和兼容性处理是跨环境 JavaScript 库开发的 “黄金标准”,其核心方法论可总结为:

  1. IIFE 封装:通过立即调用函数表达式创建独立作用域,隔离内部变量,统一全局上下文,支持多模块系统导出;
  2. 环境检测优先:“先检测、后适配”,通过多层级检测识别运行环境、模块系统、原生 API 支持情况,避免暴力降级;
  3. 原生优先策略:优先使用高性能的原生 API,无支持时提供轻量、兼容的自定义实现;
  4. 边界值处理:全面覆盖 null/undefined、DOM 节点污染、环境篡改等边界情况,确保鲁棒性;
  5. 性能与兼容平衡:通过缓存、条件分支优化、短路求值等手段,在保证兼容性的同时最大化性能;
  6. 优雅降级:所有兼容逻辑都遵循 “能跑就行→行为一致→性能最优” 的原则,避免过度兼容。

这些设计思想不仅适用于工具库开发,也可直接应用于业务代码的跨环境适配(如兼容新旧浏览器、Node.js/ 浏览器同构项目)。通过学习 Lodash 的兼容性处理机制,你能构建出更健壮、更通用的 JavaScript 代码,同时深入理解 JavaScript 生态的历史演进和环境差异。

用 AI Elements Vue 在 Vue/Nuxt 里快速搭一个“AI 对话 + 推理 + 引用 + 工具调用”的 UI

用 AI Elements Vue 在 Vue/Nuxt 里快速搭一个“AI 对话 + 推理 + 引用 + 工具调用”的 UI

目标读者:开发者 / 运维 / 架构师(偏实操) 文章目标:3 分钟判断值不值得试,10 分钟照着跑起来。

一、开源项目简介

图片

AI Elements Vue

一句话简介:基于 shadcn-vue 的 Vue 组件库 + 自定义组件注册表,用“拷贝进项目的组件代码”方式,快速拼装 AI 应用常见交互(对话、消息、推理、引用、工具展示等)。

  • 适合谁:Vue 3 / Nuxt 3 开发者、需要快速做 AI 产品原型的团队
  • 典型场景:
    • Web Chatbot(支持消息流式输出、推理区、来源引用)
    • AI 应用后台/工作台(任务队列、工具调用展示、模型选择)
    • 需要“可改源码组件”的 AI UI(不想被封装库限制)

二、开源协议

  • Apache-2.0

三、界面展示

官网有完整示例页与组件文档。

Prompt Input:富输入(附件、模型选择、工具按钮、提交状态等)

四、功能概述

1) 组件覆盖范围:面向 AI 应用的“可组合 UI 原语”

AI Elements Vue 的特点是:安装后组件代码会落到你的项目里(通常在 @/components/ai-elements/),你可以直接改样式/结构/逻辑。

  • 是什么:一组面向 AI 场景的 Vue 组件(对话容器、消息、推理、引用、工具调用、队列等)
  • 怎么做:通过 CLI 把组件以源码形式写入项目目录;在页面里按文档组合使用
  • 注意事项:
    • 依赖 shadcn-vue 的工程化约定(如 components.json、Tailwind 配置等)
    • 样式基于 Tailwind;文档明确提到“支持 CSS Variables mode only”(以 README 为准)

2) 与 AI/大模型交互相关的组件能力(重点)

来自 README/文档的组件清单(部分分类):

  • Chatbot
    • conversation:对话容器(滚动区域、滚动到底部按钮等)
    • message:消息容器/内容/动作区(常见的 Assistant/User 样式区分)
    • prompt-input:输入组件(支持附件、模型选择、工具区、提交按钮与状态)
    • reasoning / chain-of-thought:推理展示(可折叠、流式更新、步骤状态)
    • sources / inline-citation:来源展示/行内引用
    • tool:工具使用可视化(用于展示 tool call / tool result 这类 UI)
    • task / plan / queue:任务/计划/队列类展示(适合 Agent 类产品)
    • confirmation:工具执行确认流程(审批/确认 UI)
    • model-selector:模型选择 UI
  • Utilities
    • code-block:代码块展示(带复制等能力,具体以组件实现为准)
    • image:AI 生成图展示组件
    • loader:加载状态
  • Vibe-Coding
    • artifact:代码/文档产物展示
    • web-preview:网页预览嵌入
  • Workflow(工作流/画布)
    • canvas / node / edge / controls / toolbar 等(基于 Vue Flow 一类生态,详见技术选型)

3) 主题/样式方案:基于 shadcn-vue + Tailwind

  • 是什么:沿用 shadcn-vue 的设计系统与 Tailwind 工具类(含暗色/主题切换的常见做法)
  • 怎么做:
    • 组件内主要是 Tailwind class + CSS variables token
    • 文档提到 theme switching 依赖 data-theme 机制(以文档 Troubleshooting 为准)
  • 注意事项:
    • “为什么组件没样式”:需确保 Tailwind 4 + globals.css 引入 Tailwind 并包含 shadcn-vue base styles(以文档为准)
    • “module not found”:确保 tsconfig.json 配好 @/* alias(以文档为准)

4) 外部依赖与网络/权限

  • 安装组件时:
    • CLI 会拉取组件注册表:https://registry.ai-elements-vue.com/all.json(需要可访问外网)
    • 也可以用 shadcn-vue CLI 直接 add registry URL(同样需要外网)
  • 运行 AI 示例时:
    • 示例文档推荐 Vercel AI Gateway,并要求配置 API Key(以官网示例为准)
    • 使用 @ai-sdk/vue(AI SDK)进行消息流式交互(以文档为准)

五、技术选型

以下来自官网首页、文档、仓库 README 与锁文件信息(版本可能随仓库更新,以仓库为准)。

  • UI 基础:shadcn-vue
  • 样式:Tailwind CSS(文档提到 Tailwind CSS 4)
  • 语言:TypeScript
  • AI SDK:ai + @ai-sdk/vue(示例使用)
  • 图标:lucide-vue-next(示例使用)
  • 工作流/画布:@vue-flow/*(仓库依赖出现)
  • 代码高亮/渲染(仓库依赖出现,具体以组件为准):shikivue-stream-markdown

六、如何使用项目

下面只给“最小可跑路径”;更细参数以官网文档与 README 为准。 前置准备(建议先确认):Node.js 18+、已存在 Vue/Nuxt 项目、Tailwind 已配置、可访问外网 registry。

1) 用 AI Elements Vue CLI(推荐)

安装全部组件(最省事)

# npm / pnpm / yarn / bun 均可用 npx/dlx/x/dlx 形式
npx ai-elements-vue@latest

文档说明该命令会:

  • 如果未配置 shadcn-vue,会引导/自动处理
  • 将组件源码写入项目(通常在 @/components/ai-elements/,以你的 shadcn 配置为准)

安装指定组件(更轻量)

npx ai-elements-vue@latest add message
npx ai-elements-vue@latest add conversation
npx ai-elements-vue@latest add code-block
npx ai-elements-vue@latest add chain-of-thought

2) 用 shadcn-vue CLI 走 registry(适合已有 shadcn 工作流)

# 安装全部
npx shadcn-vue@latest add https://registry.ai-elements-vue.com/all.json

# 安装单个组件(示例:message)
npx shadcn-vue@latest add https://registry.ai-elements-vue.com/message.json

3) 最小示例:渲染消息列表(Vue SFC)

来自官网 Usage 页面(按其示例组织,路径以你的实际安装目录为准):

<script setup lang="ts">
import { useChat } from '@ai-sdk/vue'
import {
  Message,
  MessageContent,
  MessageResponse,
} from '@/components/ai-elements/message'

const { messages } = useChat()
</script>

<template>
  <div>
    <Message
      v-for="(msg, index) in messages"
      :key="index"
      :from="msg.role"
    >
      <MessageContent>
        <template v-for="(part, i) in msg.parts">
          <MessageResponse
            v-if="part.type === 'text'"
            :key="`${msg.role}-${i}`"
          >
            {{ part.text }}
          </MessageResponse>
        </template>
      </MessageContent>
    </Message>
  </div>
</template>

对应依赖(示例页给的是 AI SDK):

# npm
npm i ai @ai-sdk/vue zod

# pnpm
pnpm i ai @ai-sdk/vue zod

注意:这只是“前端渲染消息”示例。真正的对话需要你提供后端接口/路由来把用户输入转给模型并返回流式输出。

4) 10 分钟跑起来:Nuxt Chatbot 示例

按官网 Examples -> Chatbot 的步骤(这里保留最小命令,细节以官网为准):

pnpm create nuxt@latest ai-chatbot
cd ai-chatbot

然后按 shadcn-vue 的 Nuxt 安装文档完成 Tailwind + 模块配置(官网给了链接,需按你的项目情况调整)。

安装 AI Elements Vue:

pnpm dlx ai-elements-vue@latest

安装 AI SDK:

pnpm i ai @ai-sdk/vue zod

启动:

pnpm run dev

默认访问(Nuxt 默认端口):

  • http://localhost:3000

示例里还包含:.env 配置 AI Gateway Key、server/api/chat.ts API route、pages/index.vue UI 组合(Reasoning/Sources/PromptInput/Conversation 等)。如果你要完整复现,请直接照官网示例页复制对应文件内容。

七、二次开发注意事项

环境依赖

  • Node.js:18+(官网 Introduction 明确)
  • Vue:目标 Vue 3(文档提到 “built targeting Vue 3”)
  • Tailwind:Tailwind CSS 4(文档提到)
  • 需要 AI 能力时:ai@ai-sdk/vue(示例使用)
  • 需要 shadcn-vue:项目需初始化(README 提到 npx shadcn-vue@latest init

具体版本号会随仓库更新,建议以 package.json / lockfile 为准。

本地开发与改组件的方式

  • 组件不是“黑盒依赖”,而是写入你的项目目录:
    • 默认目录:@/components/ai-elements/(文档说明可由 shadcn 配置决定)
  • 重新运行安装命令时:
    • CLI 会在覆盖文件前询问(文档说明),避免你本地改动被静默覆盖

常见问题

  • 组件没样式:检查 Tailwind 4 与 globals.css 是否正确引入 Tailwind 并包含 shadcn-vue base styles(以文档为准)
  • CLI 跑了但没写入文件:确认当前目录是项目根目录(有 package.json),并检查 components.json 配置(以文档为准)
  • “module not found”:检查路径别名 @/tsconfig.jsonpaths 配置)(以文档为准)

八、目录结构与主要文件

这里以仓库 README 的贡献说明与文档“Edit this page”路径推断关键目录;细节以仓库实际结构为准。

.
├── apps/
│   ├── www/                  # 官网文档站点(Nuxt)
│   └── registry/             # registry / MCP 相关服务(以仓库为准)
├── packages/
│   ├── elements/             # 组件实现源码(核心)
│   ├── cli/                  # CLI(npx ai-elements-vue)
│   ├── examples/             # 示例组件/组合(以仓库为准)
│   └── shadcn-vue/           # 关联的 shadcn-vue 工作区包(以仓库为准)
└── LICENSE                   # Apache-2.0

九、源码地址

  • 官网:www.ai-elements-vue.com/
  • GitHub:github.com/vuepont/ai-…
  • NPM(README 有 badge):ai-elements-vue(以 NPM 页面为准)
  • 组件 registry:
    • 全量:https://registry.ai-elements-vue.com/all.json
    • 单组件示例:https://registry.ai-elements-vue.com/message.json
    • MCP:https://registry.ai-elements-vue.com/mcp

Xsens为拳击康复训练带来运动数据支持

随着Saxion大学应用科技系将Xsens运动捕捉与压力传感器数据相结合,量化并可视化康复过程与支持更好的恢复训练决策,让拳击运动员受伤恢复训练变得越来越可预测。

挑战:

拳击康复很难用传统的手动方法进行跟踪,并且常用工具不能以特定于运动的方式捕捉拳和踢技的表现。

解决方案:

Saxion团队使用Xsens Link suit和pad上的压力传感器,记录结构化会话期间的全身运动和击打输出,以生成客观的恢复指标,如左右对称。

关键要点:

专为真实训练环境打造:惯性运动捕捉不需要工作室,这使得每周的康复测量切实可行。

运动加输出:Xsens提供全身动作捕捉,而压力传感器则负责量化冲击所造成的影响,将技术与结果联系起来。

清除进度信号:对称性和可重复的尽力出拳分析等指标有助于理疗师跟踪随时间推移的伤病改善情况,并指导康复训练的决策。

拳击是一项危险运动。虽然看起来令人兴奋,但投掷上钩拳、刺拳和回旋踢很容易导致运动员受伤。为了更好地理解和改善这一高要求领域的康复训练流程,Saxion大学应用科技系的研究人员正在探索监测康复治疗的新方法。

Saxion UFC Figther 2.webp.png

缩小拳击康复研究的误差

“关于拳击康复的研究并不多,尤其是关于康复监测技术的使用,”研究员兼人体运动科学家 Katrien Fischer 说。 “传统的分析方法是相当手动的,这使得它们很难量化和分析,因为这些挑战并不是针对特定运动的。”

在传统评估中通常使用地面反作用力板对运动员进行评估,这在跑步、足球或跳远等运动中非常有效。然而,这些工具在应用于拳打脚踢至关重要的格斗运动中时却显得不足。这一差距凸显了针对拳击独特要求的更专业方法的需求。

“拳击技术训练和拳击康复对于该运动很重要,”研究员兼物理治疗师 Remco Kuipers 补充道。 “通常情况下,我们并没有做太多事情。拳手们往往会克服痛苦,专注于为下一场比赛做好准备。”这种有限的康复使得格斗运动中的恢复测量变得困难——Saxion团队希望改变这一情况。

通过惯性动作捕捉进行全身运动学分析

为了应对这一挑战,研究人员使用了 Xsens 动作捕捉技术。 “我们知道 Xsens 是正确的选择,因为我们过去曾使用过该系统,”Katrien 解释道。 “我们选择 Link 套装是因为它的延迟低并且能够捕捉到每一个细微差别。现在,我们每周都会使用它为运动员进行评估。”

ScreenShot_2026-01-16_152831_837.png

Xsens Link 套装是一个全身动捕系统,带有内置传感器,可跟踪关节旋转、角度和速度。与基于相机的设置不同,它不需要专用的工作室空间,因此可以在任何地方轻松使用。对于训练日程繁忙的运动员来说,这种便利性使康复课程变得更容易、更省时。

惯性运动捕捉还允许在灵活的地点进行康复训练,而不需要专门的实验室。研究人员和物理治疗师可以将技术直接带给运动员,从而实现高效、高质量的现场分析。

“我们开发了一个详细的、结构化的动作捕捉流程,以保持数据收集的一致性,”Remco说。“它包括与专业教练一起进行影子拳击和护垫热身。然后我们测量了八次拳击训练,每次训练之间休息一分钟。我们要求受试者以最大的幅度击打护垫,然后采取五次刺拳、交叉拳、勾拳和上勾拳进行分析。”

ScreenShot_2026-01-16_152909_967.png

从主观反馈到客观康复决策

通过将运动捕捉数据与压力传感器测量相结合,研究人员可以对运动员表现产生可量化的见解。他们正在探索的一种方法是肢体对称指数(LSI),它将一只手臂或拳击的表现与另一只手臂或拳击的表现进行比较。该方法使用嵌入在垫上的压力传感器测量数据,提供了一种精确的方法来测量特定动作中的力量和协调性。

这种客观数据将改变格斗运动的康复流程。物理治疗师可以将恢复计划建立在可衡量的进展的基础上,而无需主要依赖于主观感受,从而为运动员带来更好的结果。

“在这个项目中使用 Xsens 将为Saxion和整个物理治疗行业带来新的发现,”Remco 总结道。

vscode 中找settings.json 配置

在VSCode中查找和配置settings.json,最快捷的方式是通过命令面板直接打开,具体操作如下:

一、快速打开settings.json的方法

方法1:命令面板(推荐)

  1. Ctrl + Shift + P(Windows/Linux)或 Cmd + Shift + P(macOS)
  2. 输入"Preferences: Open Settings (JSON)"并回车
  3. 系统会直接打开当前生效的settings.json文件(通常是用户全局设置)

方法2:设置界面跳转

  1. Ctrl + ,打开设置UI界面
  2. 点击右上角的"打开设置(JSON)"图标(文件图标)
  3. 自动跳转到对应的JSON文件

方法3:文件路径访问

  • 用户全局设置:在资源管理器中输入对应路径(Windows:%APPDATA%\Code\User\settings.json
  • 工作区设置:项目根目录下的.vscode/settings.json(需先创建文件夹)

二、配置示例(针对"不换行整理"需求)

在打开的settings.json文件中,添加或修改以下配置:

{
  // 全局格式化设置
  "editor.formatOnSave": true,
  "editor.wordWrap": "off",
  
  // Prettier配置(如使用Prettier)
  "prettier.printWidth": 200,
  "prettier.proseWrap": "never",
  
  // 各语言默认格式化器
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[html]": {
    "editor.defaultFormatter": "vscode.html-language-features",
    "html.format.wrapLineLength": 0
  }
}

三、配置注意事项

  1. 保存生效:修改后按Ctrl + S保存,配置立即生效
  2. JSON格式:确保文件是合法的JSON格式(逗号、引号正确)
  3. 优先级:工作区设置(项目内)会覆盖用户全局设置
  4. 扩展依赖:如果配置了Prettier等格式化器,需确保已安装对应扩展

四、验证配置是否生效

  1. 打开一个代码文件
  2. Ctrl + S保存,观察是否按预期格式化(不自动换行)
  3. 或手动按Shift + Alt + F格式化,检查效果

如果配置后仍自动换行,可能是其他扩展或配置冲突,建议检查:

  • 是否安装了多个格式化扩展
  • 通过命令面板"Format Document With..."查看当前使用的格式化器
  • 在状态栏右下角查看当前文件使用的格式化工具

核心提示:日常使用建议通过命令面板(Ctrl+Shift+P)快速打开,这是最直接且不易出错的方式。配置时注意JSON语法正确性,保存后即可生效。

Easy (Horizontal Scrollbar) Fixes for Your Blog CSS 博客 CSS 的简易(水平滚动条)修复

下面是对文章 Easy (Horizontal Scrollbar) Fixes for Your Blog CSS(来自 aartaka.me)的精炼总结


📌 文章核心:解决博客在小屏幕上出现 横向滚动条 的 CSS 问题

作者指出,在博客或网站上,一些常见元素往往会超出视口宽度,从而触发让人不舒服的横向滚动条。为此,他给出了简单有效的 CSS 解决方案应对最常见的三类情况:


🛠️ 实用修复方法

  1. 代码块 (<pre>) 内容太宽导致滚动条 ➤ 解决方案:让代码块自身可横向滚动,而不是整页滚动。

    pre {
        overflow-x: auto;
    }
    

    这样只有代码块在必要时滚动,不会破坏页面整体布局。

  2. 图片太大,超出容器宽度 ➤ 修复办法:限制图片最大宽度为容器宽度。

    img {
        max-width: 100%;
        height: auto;
    }
    

    这会让大图缩放以适应小视口。

  3. 表格宽度过大导致横向溢出 ➤ 解决思路:将表格包装在一个允许横向滚动的容器中:

    <div class="scrollable">
      <table></table>
    </div>
    
    .scrollable {
        overflow-x: auto;
    }
    

    让表格自己滚动,而非整个页面。


🧠 额外补充:处理长单词断行

对于 极长无分隔的单词(例如德语复合词),浏览器默认可能不换行造成溢出。作者建议:

  • 在合适位置插入 <wbr> 标签,允许浏览器断行;

  • 或者用 CSS 强制换行:

    p {
        overflow-wrap: anywhere;
    }
    

    (但作者不太推荐 CSS 方案)


✨ 总结

这篇文章提供了几条不用 JavaScript、纯用 CSS即可显著改善博客在窄屏设备上的展示体验的技巧,分别针对:

  • 代码块过宽
  • 图片尺寸失控
  • 表格宽度问题
  • (加分项)超长单词换行

这些都是现代博客常见导致横向滚动条的设计问题,修好它们能让移动端和小屏设备用户体验更佳。

前端监测界面内存泄漏

前端监测界面内存泄漏通常分为开发阶段的排查(非代码方案)和自动化/生产环境的监控(代码方案)。 以下是详细的代码方案和非代码方案。 一、 非代码方案(开发与调试阶段) 主要依赖浏览器自带的开发者工具(
❌