阅读视图

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

详细说一下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即可显著改善博客在窄屏设备上的展示体验的技巧,分别针对:

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

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

前端监测界面内存泄漏

前端监测界面内存泄漏通常分为开发阶段的排查(非代码方案)自动化/生产环境的监控(代码方案)

以下是详细的代码方案和非代码方案。


一、 非代码方案(开发与调试阶段)

主要依赖浏览器自带的开发者工具(Chrome DevTools),这是最直观、最常用的方法。

1. Performance 面板(宏观监测)

用于观察内存随时间变化的趋势。

  • 操作步骤
    1. 打开 Chrome DevTools -> Performance 标签。
    2. 勾选 Memory 选项。
    3. 点击录制(Record),在页面上执行一系列操作(如:打开弹窗 -> 关闭弹窗,重复多次)。
    4. 停止录制。
  • 分析
    • 查看 JS Heap 曲线。
    • 正常情况:内存上升后,触发 GC(垃圾回收)会回落到基准线(锯齿状)。
    • 泄漏迹象:内存阶梯式上升,每次 GC 后最低点都比上一次高,说明有对象无法被回收。

2. Memory 面板 - Heap Snapshot(微观定位)

用于精确定位是什么对象泄漏了。

  • 操作步骤
    1. 打开 Memory 标签。
    2. 选择 Heap snapshot
    3. 在操作前拍一张快照(Snapshot 1)。
    4. 执行操作(如组件加载再卸载)。
    5. 再拍一张快照(Snapshot 2)。
  • 分析
    • 在 Snapshot 2 中选择 Comparison(对比)视图,对比 Snapshot 1。
    • 重点关注 Detached DOM tree(分离的 DOM 树)。这通常意味着 DOM 节点已从页面移除,但 JS 中仍有引用(如未解绑的事件监听器),导致无法回收。

3. Task Manager(任务管理器)

  • 操作:Chrome 浏览器中按 Shift + Esc
  • 作用:查看当前 Tab 页面的总体内存占用(Memory Footprint)。如果页面静止不动但数值持续上涨,说明存在泄漏。

二、 代码方案(自动化测试与线上监控)

代码方案主要用于 CI/CD 流程中的回归测试,或生产环境的异常上报。

1. 使用 Puppeteer 编写自动化检测脚本

这是目前最主流的自动化检测方案。通过模拟用户操作,并在操作前后强制执行垃圾回收,对比堆内存大小。

关键点:启动 Chrome 时需要开启 --js-flags="--expose-gc" 以便在代码中手动触发 GC。

// monitor-leak.js
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    headless: false, // 方便调试观察
    args: ['--js-flags="--expose-gc"'] // 关键:允许手动触发垃圾回收
  });
  
  const page = await browser.newPage();
  await page.goto('http://localhost:8080/target-page');

  // 1. 获取基准内存
  await page.evaluate(() => window.gc()); // 强制 GC
  const initialMemory = await page.metrics();
  console.log(`初始 JSHeapSize: ${initialMemory.JSHeapUsedSize / 1024 / 1024} MB`);

  // 2. 模拟用户操作(重复多次以放大泄漏效果)
  for (let i = 0; i < 10; i++) {
    await page.click('#open-dialog-btn');
    await page.waitForSelector('.dialog');
    await page.click('#close-dialog-btn');
    await page.waitForSelector('.dialog', { hidden: true });
  }

  // 3. 再次强制 GC 并检测
  await page.evaluate(() => window.gc());
  const finalMemory = await page.metrics();
  console.log(`操作后 JSHeapSize: ${finalMemory.JSHeapUsedSize / 1024 / 1024} MB`);

  const diff = finalMemory.JSHeapUsedSize - initialMemory.JSHeapUsedSize;
  
  // 4. 设置阈值判断(例如增长超过 1MB 视为泄漏)
  if (diff > 1024 * 1024) {
    console.error(`检测到内存泄漏! 增长量: ${diff / 1024} KB`);
  } else {
    console.log('内存使用正常');
  }

  await browser.close();
})();

2. 生产环境运行时监控 (performance.memory)

虽然 performance.memory 是非标准 API(主要 Chrome 支持),但它是线上获取内存数据的唯一低成本途径。

可以将其封装为 Hook 或工具函数,定期上报。

/**
 * 简单的内存监控上报函数
 * 建议在页面空闲时或定期执行
 */
function reportMemoryUsage() {
  // 仅 Chrome/Edge 支持
  if (performance && performance.memory) {
    const {
      jsHeapSizeLimit, // 内存大小限制
      totalJSHeapSize, // 可使用的内存
      usedJSHeapSize   // 实际使用的内存
    } = performance.memory;

    const usedMB = usedJSHeapSize / 1024 / 1024;
    
    console.log(`当前内存使用: ${usedMB.toFixed(2)} MB`);

    // 设置报警阈值,例如超过 50MB 或 占比过高时上报
    // 注意:这里的数值包含未回收的垃圾,仅作趋势参考
    if (usedMB > 50) {
      // sendToAnalytics({ type: 'memory_warning', value: usedMB });
    }
  }
}

// 示例:每 10 秒采样一次
setInterval(reportMemoryUsage, 10000);

3. 使用 Meta 的 MemLab

MemLab 是 Meta (Facebook) 开源的专门用于查找 JavaScript 内存泄漏的框架,它基于 Puppeteer,但封装了更完善的分析逻辑(自动识别 Detached DOM)。

工作流程

  1. 导航到页面。
  2. 执行操作。
  3. 返回初始状态。
  4. MemLab 自动分析快照差异,寻找未释放的对象。

配置文件示例 (memlab-scenario.js):

module.exports = {
  // 初始访问地址
  url: () => 'http://localhost:3000',

  // 交互操作:通常是触发泄漏的操作
  action: async (page) => {
    await page.click('button#trigger-action');
    // 等待操作完成
    await new Promise(r => setTimeout(r, 500));
  },

  // 回退操作:试图让页面回到初始状态
  back: async (page) => {
    await page.click('button#reset-state');
    // 等待状态恢复
    await new Promise(r => setTimeout(r, 500));
  },

  // 过滤规则:只关注特定的泄漏对象(可选)
  leakFilter: (node, snapshot, leakerRoots) => {
    // 例如只关注分离的 DOM 元素
    return node.type === 'native' && node.name.startsWith(' Detached'); 
  },
};

运行命令: memlab run --scenario memlab-scenario.js

4. 使用 FinalizationRegistry (现代浏览器 API)

用于在开发阶段通过代码精确监听某个对象是否被回收。如果对象应该被销毁但长时间未收到回调,可能存在泄漏。

// 调试工具类:用于监测组件或对象是否被回收
const registry = new FinalizationRegistry((heldValue) => {
  console.log(`✅ 对象 [${heldValue}] 已被垃圾回收`);
});

export function observeObject(obj, name) {
  registry.register(obj, name);
}

// 使用示例(例如在 Vue/React 组件中)
// mounted / useEffect:
// let heavyObject = { data: new Array(10000) };
// observeObject(heavyObject, 'MyHeavyData');
// heavyObject = null; // 解除引用,理论上应该触发上面的回调

总结建议

  1. 日常开发:遇到页面卡顿或 Crash,首选 Chrome DevTools Memory 面板 抓快照对比,重点查 Detached DOM
  2. 持续集成:引入 MemLab 或编写 Puppeteer 脚本,针对关键核心链路(如长时间驻留的单页应用路由切换)进行回归测试。
  3. 线上监控:利用 performance.memory 进行粗粒度的趋势监控,结合错误监控平台(如 Sentry)排查 OOM(Out of Memory)崩溃。

前端向架构突围系列 - 工程化(一):JavaScript 演进史与最佳实践

第一篇:基石——JavaScript 模块化演进史与现代最佳实践

写在最前

如果把前端工程化体系比作一座摩天大楼,那么 模块化(Modularization) 就是这座大楼的钢结构。没有它,Webpack 配得再花哨,架构设计得再宏大,终究也只是用“全局变量”这种泥巴堆砌起来的危房。

很多同学对模块化的理解还停留在“怎么写 importexport”的语法层面。但在架构师的眼里,模块化解决的是两个更本质的问题:代码的物理边界(封装)代码的依赖关系(治理)

这一节,我们不谈过时的语法细节,而是从架构演进的视角,复盘从 IIFE 到 ESM 的生死局,深挖 Node.js 与浏览器环境割裂的深层原因,并给出在 Vite/Rspack 时代下,设计模块体系的“终极答案”。

image.png


一、 混沌与突围:史前时代的架构挣扎

在 Node.js 诞生之前,JavaScript 在浏览器里更像是一种“胶水语言”。那时候没有模块,只有全局变量

1.1 命名空间模式:脆弱的秩序

为了避免 var user = 'admin' 这种代码在不同文件中互相覆盖,早期的架构师们模仿 Java,搞出了 Namespace(命名空间) 模式。雅虎的 YUI 是那个时代的集大成者:

var MY_APP = {};
MY_APP.utils = {};
MY_APP.utils.format = function() { /* ... */ };

这种写法的本质,依然是在那个巨大的 window 对象上挂载属性。它只是把“散落的垃圾”归拢到了几个“大的垃圾桶”里,并没有解决依赖管理的问题——你依然需要手动维护 <script> 标签的顺序。

1.2 IIFE:闭包的胜利

为了实现真正的“私有化”,IIFE (立即执行函数表达式) 成为了当时的架构标准。

var Module = (function($) {
    var privateState = 'secret'; // 真正的私有变量

    function _internal() { /*...*/ }

    return {
        init: function() {
            _internal();
            $('body').addClass('ready');
        }
    };
})(jQuery); // -> 这里显式声明了依赖

架构反思: IIFE 虽然完美解决了封装性,但它对依赖治理几乎是束手无策的。 想象一下,当你的项目有 50 个文件,你必须人肉保证 jquery.jsplugin.js 之前加载,core.jsapp.js 之前加载。一旦依赖链断裂,浏览器只会冷冰冰地抛出 ReferenceError

这种“人肉依赖树”的维护成本,在 Web 应用日益复杂的背景下,成为了不可承受之重。


二、 分道扬镳:CommonJS 与 AMD 的路线之争

2009 年是 JS 历史上的奇点。Node.js 的横空出世,让 JS 第一次有了脱离浏览器生存的能力。

2.1 CommonJS:服务端的同步哲学

Ryan Dahl 在设计 Node.js 时,采用了 CommonJS (CJS) 规范。 CJS 的设计哲学非常契合服务端场景:文件在硬盘上,读取几乎是实时的,所以同步加载没问题。

// math.js
const a = 1;
module.exports = { a };

// index.js
const math = require('./math'); // 代码执行到这里会暂停,直到文件加载完
console.log(math.a);

CJS 的最大贡献,是它确立了 DAG (有向无环图) 的依赖模型。这是现代工程化的雏形。

2.2 浏览器的反击:异步的 AMD

但是,CJS 没法直接用在浏览器。为什么? 因为浏览器加载文件走的是网络(Network)。如果像 CJS 那样 require() 一个文件就卡住主线程等待网络响应,页面早就卡死了。

于是,AMD (RequireJS) 应运而生。它强制要求依赖前置异步加载

// 这种回调地狱般的写法,是很多老前端的噩梦
define(['jquery', './utils'], function($, utils) {
    return {
        start: function() { ... }
    };
});

历史的尘埃: 站在今天回看,AMD 和 CMD(Sea.js,淘宝玉伯的大作)都是特定历史时期的“过渡产物”。它们通过“函数包裹”来模拟模块化,这种 Wrapper 既丑陋,又带来了额外的运行时开销。

我们需要一种语言层面的、标准化的解决方案。


三、 大一统:ES Modules 的静态革命

ECMAScript 2015 (ES6) 终于带来了官方标准——ES Modules (ESM)。 请注意,ESM 战胜 CJS 不仅仅是因为它是“官方标准”,更因为它是“静态”的。 这决定了现代构建工具(Webpack/Rollup/Vite)的上限。

3.1 动态 vs 静态:Tree Shaking 的物理基础

看两个例子:

  • CommonJS (动态):

    const path = './' + (isDev ? 'dev' : 'prod');
    const module = require(path); // 运行时才能确定引用了谁
    
  • ES Modules (静态):

    import { func } from './utils'; // 编译时必须确定路径
    // 路径不能是变量,import 必须在顶层
    

正是因为 ESM 这种看似“死板”的静态限制,让构建工具在代码运行之前就能分析出完整的依赖图谱。 这直接催生了 Tree Shaking(摇树优化) ——如果工具分析出你只 importButton,那么 Table 组件的代码在打包时就会被物理剔除。

架构师视角: 如果你的公司内部组件库还在大量使用 CJS 导出(module.exports),那你实际上是在阻碍业务方进行性能优化。ESM 是现代前端工程化的入场券。


四、 阵痛期:CJS 与 ESM 的割裂与互通

这可能是目前前端基建中最让人头疼的部分。 Node.js 生态长期被 CJS 占据,而新兴的生态(Vite, Rollup)全力拥抱 ESM。我们正处在一个新旧交替的“地震带”上。

4.1 "Dual Package Hazard"(双包隐患)

很多库作者为了兼容,会同时发布 CJS 和 ESM 版本。 如果你的项目里,A 依赖使用了 CJS 版本的 package-x,而 B 依赖使用了 ESM 版本的 package-x,会发生什么?

Node.js 会把它们视为两个独立的模块

  • 结果:代码体积翻倍。
  • 灾难:instanceof 检查失效,单例模式破功(因为内存里有两个单例)。

4.2 解决方案:Node.js 的条件导出

package.json 中,现代库应该使用 exports 字段来精确控制不同环境的入口,而不是简单的 main 字段:

{
  "name": "my-library",
  "type": "module", // 默认视作 ESM
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs", // 只有 ESM 环境(Vite/Webpack5+)能读到
      "require": "./dist/index.cjs"  // 老旧 CJS 环境读到
    }
  }
}

4.3 互操作的深坑

  • ESM 引用 CJS:比较顺滑,Node 会自动处理。
  • CJS 引用 ESM这是地狱。 由于 ESM 支持 Top-level Await,本质上它是异步的。而 CJS require 是同步的。同步无法包含异步。 所以,如果你在 CJS 项目里想用纯 ESM 的库(比如 node-fetch v3+),你只能用 await import(...),这会导致你的整个同步代码链路被“传染”成异步。

建议: 新起动的 Node.js 服务端项目,尽量直接上 ESM(设置 "type": "module"),长痛不如短痛。


五、 现代工程体系下的模块化

进入 Webpack 5、Vite 和 Rspack 的时代,模块化的意义已经超越了语法本身。

5.1 Bundless (无打包) 的真相

Vite 的快,源于利用了浏览器原生支持 ESM 的特性。

  • 传统打包:从入口开始,把所有依赖打包成一个 Bundle,再给浏览器。
  • Vite/Bundless:浏览器请求 App.jsx -> Vite 拦截 -> 简单编译为 ESM -> 返回给浏览器。

注意:Bundless 目前主要用于开发环境。在生产环境,为了减少 HTTP 请求瀑布流(Waterfall)带来的延迟,以及更好地做代码分割和混淆,Bundle(打包)依然是必须的

5.2 最佳实践:架构师的 CheckList

在设计工程体系或编写公共库时,请遵循以下原则:

  1. 拒绝 Default Export 这是一个反直觉的建议。但在工程化角度,export const (具名导出) 远优于 export default

    • 重构安全:IDE 可以自动重命名,Default Export 很难追踪。
    • Tree Shaking:Default 往往是一个大对象,很难精确拆分。
    • 一致性:强制使用者的命名与库保持一致。
  2. 避免 "Barrel Files" (全量导出陷阱) 不要写这种文件:

    // components/index.ts
    export * from './Button';
    export * from './Table';
    export * from './Chart'; // 哪怕用户只用了 Button,构建工具也可能去分析 Chart 的依赖
    

    这种写法被称为 "Barrel Files"。在大型项目中,这会导致构建性能显著下降,且容易造成循环依赖。现代工具链更推荐按需引入,或配合 unplugin-auto-import

  3. 副作用标记 (sideEffects) 在你的 npm 包 package.json 中声明 "sideEffects": false。 这相当于给了构建工具一张“免责金牌”:“如果我的函数没被用到,请直接删掉,不用担心有副作用。”这是 Tree Shaking 能否生效的关键开关。


结语:模块化的终局

从“茹毛饮血”的 IIFE,到“诸神之战”的 CJS/AMD,再到如今 ESM 的“天下一统”。JavaScript 模块化的演进史,其实就是前端从脚本脚本语言向企业级软件工程迈进的历史。

未来的趋势是什么? 是 Import Maps(浏览器原生控制依赖映射,彻底摆脱 node_modules)和 Module Federation(微前端模块共享)。

掌握了模块化,你才能看懂 node_modules 里的幽深,才能理解为什么 Vite 这么快,才能在复杂的架构选型中,不做那个“写出死代码”的人。

Next Step: 搞懂了模块化这块基石,接下来的挑战是如何管理成千上万个模块的依赖关系。下一节,我们将深入那个让所有前端工程师又爱又恨的黑洞—— 《第二篇:治理——解构 node_modules:包管理工具的底层哲学与选型》

Vue项目中使用xlsx库解析Excel文件

项目中有个需求是上传Excel实现批量导入,但是解析Excel的需要前端来实现,所以用到了xlsx库

xlsx 库是一个强大的 JavaScript 库,用于处理 Excel 文件,支持:

  • 读取 .xls.xlsx 格式
  • 写入 Excel 文件
  • 解析工作表数据
  • 支持多种数据格式转换

在项目中安装 xlsx 库:

npm install xlsx
# 或者使用 yarn
yarn add xlsx
# 或者使用 pnpm
pnpm add xlsx

核心 API

import * as XLSX from 'xlsx';

// 主要方法
XLSX.read(data, options)      // 读取 Excel 数据
XLSX.readFile(filename)       // 从文件读取
XLSX.utils.sheet_to_json()    // 工作表转 JSON
XLSX.utils.sheet_to_csv()     // 工作表转 CSV
XLSX.utils.sheet_to_html()    // 工作表转 HTML

Excel 文件读取与解析

1. 使用 FileReader 读取文件

在浏览器环境中,我们需要使用 FileReader API 来读取用户上传的文件:

const readExcelFile = (file) => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    
    reader.onload = (e) => {
      try {
        // 读取文件内容
        const data = new Uint8Array(e.target.result);
        resolve(data);
      } catch (error) {
        reject(new Error('文件读取失败'));
      }
    };
    
    reader.onerror = () => {
      reject(new Error('文件读取失败'));
    };
    
    // 以 ArrayBuffer 格式读取文件
    reader.readAsArrayBuffer(file);
  });
};

2. 解析 Excel 文件

使用 XLSX.read() 方法解析 Excel 数据:

const parseExcelData = (data) => {
  // 读取 Excel 工作簿
  const workbook = XLSX.read(data, { type: 'array' });
  
  // 获取所有工作表名称
  const sheetNames = workbook.SheetNames;
  console.log('工作表名称:', sheetNames);
  
  // 获取第一个工作表
  const firstSheetName = sheetNames[0];
  const worksheet = workbook.Sheets[firstSheetName];
  
  // 将工作表转换为 JSON
  const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
  
  return {
    workbook,
    worksheet,
    jsonData,
    sheetNames
  };
};

3. 不同数据格式的转换

// 转换为 JSON 对象(带表头)
const jsonWithHeaders = XLSX.utils.sheet_to_json(worksheet);

// 转换为 JSON 数组(不带表头)
const jsonArray = XLSX.utils.sheet_to_json(worksheet, { header: 1 });

// 转换为 CSV 字符串
const csvString = XLSX.utils.sheet_to_csv(worksheet);

// 转换为 HTML 表格
const htmlString = XLSX.utils.sheet_to_html(worksheet);

表头验证与数据提取

1. 验证表头格式

在实际应用中,我们通常需要验证 Excel 文件的表头是否符合预期格式:

const validateExcelHeaders = (jsonData, requiredHeaders) => {
  if (jsonData.length === 0) {
    throw new Error('Excel文件为空');
  }
  
  // 获取表头行(第一行)
  const headers = jsonData[0].map(header => 
    header ? header.toString().trim() : ''
  );
  
  // 检查必需表头
  const missingHeaders = requiredHeaders.filter(header =>
    !headers.includes(header)
  );
  
  if (missingHeaders.length > 0) {
    throw new Error(`缺少必需表头: ${missingHeaders.join(', ')}`);
  }
  
  return headers;
};

2. 提取数据行

const extractDataRows = (jsonData, headers) => {
  // 跳过表头行(第一行)
  const dataRows = jsonData.slice(1);
  
  return dataRows.map((row, rowIndex) => {
    const rowData = {};
    
    headers.forEach((header, colIndex) => {
      rowData[header] = row[colIndex] || '';
    });
    
    return {
      ...rowData,
      _rowNumber: rowIndex + 2 // Excel 行号(从1开始,表头为第1行)
    };
  }).filter(row => {
    // 过滤空行(所有单元格都为空)
    return Object.values(row).some(value => 
      value !== '' && value !== undefined && value !== null
    );
  });
};

3. 数据验证与清洗

const validateAndCleanData = (dataRows, validationRules) => {
  const errors = [];
  const cleanedData = [];
  
  dataRows.forEach((row, index) => {
    const rowErrors = [];
    
    // 检查每个字段
    Object.keys(validationRules).forEach(field => {
      const value = row[field];
      const rules = validationRules[field];
      
      // 必填验证
      if (rules.required && (!value || value.toString().trim() === '')) {
        rowErrors.push(`${field} 不能为空`);
      }
      
      // 类型验证
      if (value && rules.type) {
        if (rules.type === 'number' && isNaN(Number(value))) {
          rowErrors.push(`${field} 必须是数字`);
        }
        if (rules.type === 'email' && !isValidEmail(value)) {
          rowErrors.push(`${field} 格式不正确`);
        }
      }
      
      // 枚举值验证
      if (value && rules.enum && !rules.enum.includes(value)) {
        rowErrors.push(`${field} 必须是以下值之一: ${rules.enum.join(', ')}`);
      }
    });
    
    if (rowErrors.length === 0) {
      cleanedData.push(row);
    } else {
      errors.push({
        row: row._rowNumber,
        errors: rowErrors
      });
    }
  });
  
  return { cleanedData, errors };
};

用 maptalks 在 Web 上做可扩展的 2D/3D 地图渲染与交互

用 maptalks 在 Web 上做可扩展的 2D/3D 地图渲染与交互

一、开源项目简介

maptalks

一句话简介:一个开源 JavaScript 地图库,用同一套 API 同时做 2D/3D 地图渲染与交互,并支持通过插件扩展图层、交互与可视化能力。

  • 适合谁:前端开发 / GIS 可视化开发 / 需要在业务系统里嵌入地图的工程团队
  • 典型场景(≤3):
    • 在业务后台/大屏里做 2D 地图 + 可旋转/倾斜的 3D 视角展示
    • 在地图上叠加海量几何要素(点/线/面)并做交互(编辑、测量、拾取)
    • 需要把 D3 / ECharts / THREE.js 等可视化能力“挂到地图上”

官网定位:An open-source javascript library for integrated 2D/3D maps. 说明:仓库 maptalks/maptalks.js 当前也在推进 maptalks-gl(WebGL/WebGPU 驱动的新引擎),传统 maptalks 代码在仓库的 packages/maptalks 中(README 有明确说明)。

二、开源协议

  • 以仓库 LICENSE 为准(本次从 GitHub 直接抓取 LICENSE 文件未成功)。
  • 可参考仓库内的标注信息:
    • 仓库根 package.jsonlicenseMIT
    • packages/maptalks/package.jsonlicenseBSD-3-Clause(对应 maptalks@1.8.0

如果你要在公司项目中引入:建议以 packages/maptalks 对应的实际发布包与仓库 LICENSE 交叉确认。

三、界面展示(如有 UI)

maptalks 本质是 JS 地图库,不是带管理后台的“产品 UI”。这里用官网 Examples 作为效果展示(示例来自官网)。

图片

  • 2D 地图通过倾斜/旋转进入 3D 视角(官网 Gallery 示例封面图)

  • 占位:建议打开官网 Examples 看实际交互(缩放、倾斜、编辑、测量等)
  • Examples 入口:https://maptalks.org/examples/en/map/load/

四、功能概述

1) 2D/3D 一体化地图视图

  • 是什么:同一个 Map 实例即可支持 2D 视图与 3D 视角(通过 pitch/rotate 等操作在 2D 地图上“加一维”)。
  • 怎么做:
    • 初始化 new maptalks.Map(...)
    • 通过交互或 API 进行 pitch/rotate(官网 Examples 有专门条目:Pitch and rotate、Drag to pitch and rotate)
  • 注意事项(外部依赖/限制):
    • 具体 3D 能力与浏览器有关(Wiki 提到 IE9+,3D 仅 IE11;以文档为准)
    • 3D 的渲染路径可能依赖 Canvas/WebGL(取决于功能与插件)

2) 图层与数据叠加(TileLayer / VectorLayer 等)

  • 是什么:底图通常用 TileLayer(XYZ/WMTS/WMS/ArcGIS 等示例在官网都有),业务数据用几何对象与矢量图层叠加。
  • 怎么做:
    • 底图:baseLayer: new maptalks.TileLayer('base', { urlTemplate, subdomains, attribution })
    • 业务要素:Marker/LineString/Polygon 等几何对象(官网 Examples “Geometry” 章节)
  • 注意事项:
    • 底图 urlTemplate 常是外部瓦片服务(需要网络可达、HTTPS、可能存在访问策略/CORS)
    • WMTS/WMS/ArcGIS 等通常需要你掌握服务端参数与坐标系(官网 Examples 有专章)

3) 丰富几何类型与样式系统(Symbol)

  • 是什么:内置点/线/面、集合、多几何、圆/椭圆/扇形、曲线等;样式参考受 CartoCSS 启发,支持 pattern fill、gradient、SVG icon、复合符号等(官网 Feature 描述 + Examples “Geometry Styles”)。
  • 怎么做:
    • 创建几何对象后设置 symbol(具体字段建议直接对照 Symbol Guide)
    • Symbol Guide:https://github.com/maptalks/maptalks.js/wiki/Symbol-Reference
  • 注意事项:
    • 复杂样式或大数据量叠加时,建议先评估渲染路径(Canvas2D/WebGL)与性能

4) 交互组件(绘制/编辑/测量/拾取)

  • 是什么:官网 Feature 明确提到内置 DrawTool/Editor/MeasureTool;Examples 里也有大量交互示例(编辑点/线/面、测距/测面、鼠标拾取、mouseover 高亮等)。
  • 怎么做:
    • 直接参考官网 Examples “User Interactions” 与 “Control and UIComponents”
  • 注意事项:
    • 若业务需要自定义交互(例如框选、吸附、联动),通常建议基于事件系统与插件机制扩展(Wiki 有插件开发专题)

5) 插件生态与扩展

  • 是什么:maptalks 主库提供基础地图与几何能力;更多能力通过插件扩展(官网主页列出了 Mapbox GL / THREE / D3 / ECharts 等插件入口)。
  • 怎么做:
    • 从官网 Plugins 页挑选:https://maptalks.org/plugins.html
    • 或按 Wiki 的“Plugin Develop”章节自定义图层/控件/工具
  • 注意事项:
    • 插件质量与维护状态不一;引入前建议先跑通官方示例并看最后更新时间/兼容版本

6) 性能与渲染

  • 是什么:官网提到“Canvas 2D 可流畅渲染 10k 级几何,WebGL 可到 1000k 级”(原文描述)。
  • 怎么做:
    • 从你的数据量级出发,先用官方示例验证:同类型几何、同样式复杂度、同交互频率
  • 注意事项:
    • 性能瓶颈往往来自:样式过复杂、频繁更新、事件拾取范围过大、底图/资源加载慢

五、技术选型

  • 核心库:JavaScript / TypeScript
  • 渲染:Canvas 2D + WebGL(以及 maptalks-gl 的 WebGL/WebGPU 路线)
  • 分发方式:CDN(unpkg/jsDelivr)+ npm
  • 生态集成:mapbox-gl-js · THREE.js · D3 · ECharts(插件方向)

六、如何使用项目

下面给两条“最小可跑路径”:CDN(最快)与 npm(更适合工程化)。 如果你要用 maptalks-gl(新引擎),也给一条最小示例(来自仓库 README)。

1) 直接用 CDN(5 分钟跑起来)

准备:一个空目录 + 新建 hello.html,把下面内容粘贴进去即可(Getting Started 原样整理)。

<!DOCTYPE>
<html>
<head>
  <meta charset="UTF-8" />
  <title>Maptalks Quick Start</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maptalks/dist/maptalks.css">
  <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/maptalks/dist/maptalks.min.js"></script>
</head>
<body>
  <div style="width:800px;height:600px;" id="map"></div>

  <script type="text/javascript">
    var map = new maptalks.Map('map', {
      center: [0, 0],
      zoom: 2,
      baseLayer: new maptalks.TileLayer('base', {
        urlTemplate : 'http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',
        subdomains  : ['a','b','c','d'],
        attribution : '&copy; <a href="http://www.osm.org/copyright">OSM</a> contributors, ' +
                      '&copy; <a href="https://carto.com/attributions">CARTO</a>'
      })
    });
  </script>
</body>
</html>
  • 打开方式:直接用浏览器打开 hello.html
  • 默认依赖:
    • maptalks 静态资源来自 cdn.jsdelivr.net
    • 底图瓦片来自 basemaps.cartocdn.com(需要网络可达;建议改成你自己的瓦片服务/内网瓦片)

2) npm 安装(工程化引入)

准备:已安装 Node.js 与包管理器(npm/yarn/pnpm 均可;以你的工程为准)。

安装(来自 Wiki):

npm install maptalks --save

ES Modules 用法(来自 Wiki;你的构建工具需支持 ESM):

import * as maptalks from 'maptalks';

const map = new maptalks.Map('map', {
  center: [0, 0],
  zoom: 1
});
  • 关键点:
    • 容器 #map 必须有明确宽高(否则常见现象:白屏但不报错)
    • 底图层(baseLayer)是否配置,决定你是否能“看到地图背景”

更完整的工程脚手架命令(Vite/webpack/Next.js 等)在 Wiki/社区里都有不同写法;这里不做猜测,建议以你的框架文档为准。

3) 如果你关注纯 WebGL/WebGPU 引擎:maptalks-gl(可选)

说明:仓库 README 提到 maptalks 正在升级到 maptalks-gl。最小安装与示例(来自 README):

npm i maptalks-gl
# or yarn add maptalks-gl
# or pnpm i maptalks-gl
import { Map, GroupGLLayer, VectorTileLayer } from 'maptalks-gl';

const map = new Map('map', { center: [0, 0], zoom: 2 });

const vtLayer = new VectorTileLayer('vt', {
  urlTemplate: 'http://tile.maptalks.com/test/planet-single/{z}/{x}/{y}.mvt'
});

new GroupGLLayer('group', [vtLayer]).addTo(map);
  • 外部依赖:示例里 tile.maptalks.com 是外部测试服务;建议替换为自有源
  • 可选解码器:README 提到 draco/ktx2 等需要额外引入 @maptalks/transcoders.*

七、二次开发注意事项

环境依赖(以仓库为准)

从仓库 README 与配置能看到两套信息(建议以实际开发目标包为准):

  • 仓库 README(maptalks-gl 相关):
    • Node.js 最低版本:18.16.1(README 明确写)
    • 包管理器:pnpm@9.x(README 明确写)
  • 仓库根 package.json
    • engines.node: 22
    • packageManager: pnpm@10.4.1
  • packages/maptalks/package.json(传统 maptalks 包):
    • 构建:rollup
    • 测试:karma + mocha

结论:这个仓库是多包工作区(workspaces),你改哪个包,就以哪个包目录的脚本与版本要求为准。

本地开发与构建(仓库 README 提供的通用流程)

pnpm i
pnpm build

调试(README 描述为:在你要调试的 package 根目录执行 watch/dev):

pnpm run dev

常见坑(≤3)

  • 地图容器无高度:#map 没设置高度/父容器高度为 0,页面看起来像“没渲染”
  • 资源与瓦片混用 HTTP/HTTPS:页面走 HTTPS,但 urlTemplate 还是 HTTP,浏览器会拦截(建议统一 HTTPS)
  • 外部瓦片/数据源不可达:示例默认用公网服务;上线或内网环境需要替换为自有服务并考虑访问控制/CORS

八、目录结构与主要文件(可选)

目录来自 GitHub 仓库列表(只列关键项;更细以仓库为准)。

.
├── packages/                 # 多包工作区(包含 maptalks 与 gl 相关包)
│   └── maptalks/             # 传统 maptalks 主库(README 提到“旧源码在此”)
├── build/                    # 构建/测试相关配置(karma/rollup 等)
├── debug/                    # 调试相关内容(以仓库实际为准)
├── package.json              # 工作区脚本与工程约束(node/pnpm/构建入口)
├── pnpm-workspace.yaml       # pnpm workspace 配置
├── pnpm-lock.yaml            # 依赖锁定
├── turbo.json                # turbo 构建编排(monorepo 常见)
└── README.md                 # 仓库说明(含 maptalks-gl 进展与用法)

九、源码地址

  • 官网:https://maptalks.org/
  • GitHub(主仓库,包含多个包):https://github.com/maptalks/maptalks.js
  • Examples(官网示例页入口):https://maptalks.org/examples/en/map/load/
  • Wiki(安装、环境、插件开发等):https://github.com/maptalks/maptalks.js/wiki

⏰前端周刊第 448 期(2026年1月4日-1月10日)

📢 宣言每周更新国外论坛的前端热门文章,推荐大家阅读/翻译,紧跟时事,掌握前端技术动态,也为写作或突破新领域提供灵感~

欢迎大家访问:github.com/TUARAN/fron… 顺手点个 ⭐ star 支持,是我们持续输出的续航电池🔋✨!

在线网址:frontendweekly.cn/

微信图片_20260116153048_546.png


💬 推荐语

本期围绕“工程依赖、平台边界与新能力落地”。Web 开发部分从 Web 依赖体系的结构性问题、浏览器 API 与 Web API 的边界谈起,延伸到反框架主义(优先原生 Web API)、AI 时代的界面与交互范式(AG-UI),以及 Tailwind 团队在 AI 影响下的组织变化观察;同时补上按钮这一“最基础控件”的工程化最佳实践。工具与 Demo 则一边回顾 ViteLand 生态动态、讨论 npm 的潜在安全强化路径与 Bun 的基准表现,一边用加速度计动画与可无限平移画布、像素到体素的视频特效给到可直接复用的灵感。CSS 与 JavaScript 栏目覆盖 anchor positioning、@scope、视图过渡与 Temporal 等新能力,以及一则 jsPDF 高危漏洞提醒,适合年初做一次“能力盘点 + 工程卫生”更新。


🗂 本期精选目录

🧭 Web 开发

🛠 工具

🧪 Demo

🎨 CSS

💡 JavaScript

❌