阅读视图

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

智能图像创作模型Seedream 5.0 Lite发布

36氪获悉,2月13日,字节跳动Seed团队推出Seedream 5.0 Lite智能图像创作模型。相比4.0版本,模型在理解、推理和生成方面全面提升。作为通向统一多模态模型的进一步探索,它开始能像人类设计师一样“理解”用户指令背后的意图,“看懂”不同画面中的规律,并将世界知识应用于图像与文本创作中。Seedream模型还引入了实时检索增强能力,它可通过联网检索,获取最新的知识和资讯,更精准地回应具有时效性的创作需求。

MOVA TPEAK宣布签订亿元订单

36氪获悉,近日,MOVA TPEAK宣布其Clip Pro开放式耳夹耳机拿下亿元意向订单。据了解,基于个人AI助理终端的定位,研发团队构建了一套“端云协同”的AI架构,通过高效的本地预处理与云端智能响应的结合,优化交互链路,可将AI助手整体响应速度提升至10倍,实现“即问即答”的对话体验。

Vue3 子传父全解析:从基础用法到实战避坑

在 Vue3 开发中,组件通信是绕不开的核心场景,而子传父作为最基础、最常用的通信方式之一,更是新手入门必掌握的知识点。不同于 Vue2 的 $emit 写法,Vue3 组合式 API(<script setup>)简化了子传父的实现逻辑,但也有不少细节和进阶技巧需要注意。

本文将抛开 TypeScript,用最通俗的语言 + 可直接复制的实战代码,从基础用法、进阶技巧、常见场景到避坑指南,全方位讲解 Vue3 子传父,新手看完就能上手,老手也能查漏补缺。

一、核心原理:子组件触发事件,父组件监听事件

Vue3 子传父的核心逻辑和 Vue2 一致:子组件通过触发自定义事件,将数据传递给父组件;父组件通过监听该自定义事件,接收子组件传递的数据

关键区别在于:Vue3 <script setup> 中,无需通过 this.$emit 触发事件,而是通过 defineEmits 声明事件后,直接调用 emit 函数即可,语法更简洁、更直观。

先记住核心流程,再看具体实现:

  1. 子组件:用 defineEmits 声明要触发的自定义事件(可选但推荐);
  2. 子组件:在需要传值的地方(如点击事件、接口回调),调用 emit('事件名', 要传递的数据)
  3. 父组件:在使用子组件的地方,通过 @事件名="处理函数" 监听事件;
  4. 父组件:在处理函数中,接收子组件传递的数据并使用。

二、基础用法:最简洁的子传父实现(必学)

我们用一个「子组件输入内容,父组件实时显示」的简单案例,讲解基础用法,代码可直接复制到项目中运行。

1. 子组件(Child.vue):声明事件 + 触发事件

<template>
  <div class="child">
    <h4>我是子组件</h4>
    <!-- 输入框输入内容,触发input事件,传递输入值 -->
    <input 
      type="text" 
      v-model="childInput" 
      @input="handleInput"
      placeholder="请输入要传递给父组件的内容"
    />
    <!-- 按钮点击,传递固定数据 -->
    <button @click="handleClick" style="margin-top: 10px;">
      点击向父组件传值
    </button>
  </div>
</template>

<script setup>
// 1. 声明要触发的自定义事件(数组形式,元素是事件名)
// 可选,但推荐声明:增强代码可读性,IDE会有语法提示,避免拼写错误
const emit = defineEmits(['inputChange', 'btnClick'])

// 子组件内部数据
const childInput = ref('')

// 输入框变化时,触发事件并传递输入值
const handleInput = () => {
  // 2. 触发事件:第一个参数是事件名,第二个参数是要传递的数据(可选,可多个)
  emit('inputChange', childInput.value)
}

// 按钮点击时,触发事件并传递固定对象
const handleClick = () => {
  emit('btnClick', {
    name: '子组件',
    msg: '这是子组件通过点击按钮传递的数据'
  })
}
</script>

2. 父组件(Parent.vue):监听事件 + 接收数据

<template>
  <div class="parent">
    <h3>我是父组件</h3>
    <p>子组件输入的内容:{{ parentMsg }}</p>
    <p>子组件点击传递的数据:{{ parentData }}</p>
    
    <!-- 3. 监听子组件声明的自定义事件,绑定处理函数 -->
    <Child 
      @inputChange="handleInputChange"
      @btnClick="handleBtnClick"
    />
  </div>
</template>

<script setup>
// 引入子组件
import Child from './Child.vue'
import { ref, reactive } from 'vue'

// 父组件接收数据的容器
const parentMsg = ref('')
const parentData = reactive({
  name: '',
  msg: ''
})

// 4. 处理子组件触发的inputChange事件,接收传递的数据
const handleInputChange = (val) => {
  // val 就是子组件emit传递过来的值(childInput.value)
  parentMsg.value = val
}

// 处理子组件触发的btnClick事件,接收传递的对象
const handleBtnClick = (data) => {
  // data 是子组件传递的对象,直接解构或赋值即可
  parentData.name = data.name
  parentData.msg = data.msg
}
</script>

3. 核心细节说明

  • defineEmits 是 Vue3 内置的宏,无需导入,可直接使用;
  • emit 函数的第一个参数必须和 defineEmits 中声明的事件名一致(大小写敏感),否则父组件无法监听到;
  • emit 可传递多个参数,比如 emit('event', val1, val2),父组件处理函数可对应接收 (val1, val2) => {}
  • 父组件监听事件时,可使用 @事件名(简写)或 v-on:事件名(完整写法),效果一致。

三、进阶用法:优化子传父的体验(实战常用)

基础用法能满足简单场景,但在实际开发中,我们还会遇到「事件校验」「双向绑定」「事件命名规范」等需求,这部分进阶技巧能让你的代码更规范、更健壮。

1. 事件校验:限制子组件传递的数据类型

通过 defineEmits 的对象形式,可对事件传递的数据进行类型校验,避免子组件传递错误类型的数据,提升代码可靠性(类似 props 校验)。

<script setup>
// 对象形式声明事件,key是事件名,value是校验函数(参数是子组件传递的数据,返回boolean)
const emit = defineEmits({
  // 校验inputChange事件传递的数据必须是字符串
  inputChange: (val) => {
    return typeof val === 'string'
  },
  // 校验btnClick事件传递的数据必须是对象,且包含name和msg属性
  btnClick: (data) => {
    return typeof data === 'object' && 'name' in data && 'msg' in data
  }
})

// 若传递的数据不符合校验,控制台会报警告(不影响代码运行,仅提示)
const handleInput = () => {
  emit('inputChange', 123) // 传递数字,不符合校验,控制台报警告
}
</script>

2. 双向绑定:v-model 简化子传父(高频场景)

很多时候,子传父是为了「修改父组件的数据」,比如表单组件、开关组件,这时可使用 v-model 简化代码,实现父子组件双向绑定,无需手动声明事件和处理函数。

Vue3 中,v-model 本质是「语法糖」,等价于 :modelValue="xxx" @update:modelValue="xxx = $event"

优化案例:子组件开关,父组件显示状态

<!-- 子组件(Child.vue) -->
<template>
  <div class="child">
    <h4>子组件开关</h4>
    <button @click="handleSwitch">
      {{ isOpen ? '关闭' : '打开' }}
    </button>
  </div>
</template>

<script setup>
// 1. 接收父组件通过v-model传递的modelValue
const props = defineProps(['modelValue'])
// 2. 声明update:modelValue事件(固定命名,不可修改)
const emit = defineEmits(['update:modelValue'])

// 子组件内部使用父组件传递的值
const isOpen = computed(() => props.modelValue)

// 开关切换,触发事件,修改父组件数据
const handleSwitch = () => {
  emit('update:modelValue', !isOpen.value)
}
</script>
<!-- 父组件(Parent.vue) -->
<template>
  <div class="parent">
    <h3>父组件:{{ isSwitchOpen ? '开关已打开' : '开关已关闭' }}</h3>
    <!-- 直接使用v-model,无需手动监听事件 -->
    <Child v-model="isSwitchOpen" />
  </div>
</template>

<script setup>
import Child from './Child.vue'
import { ref } from 'vue'

const isSwitchOpen = ref(false)
</script>

扩展:多个 v-model 双向绑定

Vue3 支持给同一个子组件绑定多个 v-model,只需给 v-model 加后缀,对应子组件的propsemit 即可。

<!-- 父组件 -->
<Child 
  v-model:name="parentName" 
  v-model:age="parentAge" 
/>

<!-- 子组件 -->
<script setup>
// 接收多个v-model传递的props
const props = defineProps(['name', 'age'])
// 声明对应的update事件
const emit = defineEmits(['update:name', 'update:age'])

// 触发事件修改父组件数据
emit('update:name', '新名字')
emit('update:age', 25)
</script>

3. 事件命名规范:提升代码可读性

在实际开发中,遵循统一的事件命名规范,能让团队协作更高效,推荐以下规范:

  • 事件名采用「kebab-case 短横线命名」(和 HTML 事件命名一致),比如 input-change 而非 inputChange
  • 事件名要语义化,体现事件的用途,比如 form-submit(表单提交)、delete-click(删除点击);
  • 双向绑定的事件固定为 update:xxx,xxx 对应 props 名,比如 update:nameupdate:visible

四、实战场景:子传父的常见应用

结合实际开发中的高频场景,给大家补充 3 个常用案例,覆盖大部分子传父需求。

场景1:子组件表单提交,父组件接收表单数据

<!-- 子组件(FormChild.vue) -->
<template>
  <div class="form-child">
    <input v-model="form.name" placeholder="请输入姓名" />
    <input v-model="form.age" type="number" placeholder="请输入年龄" />
    <button @click="handleSubmit">提交表单</button>
  </div>
</template>

<script setup>
import { reactive } from 'vue'

const emit = defineEmits(['form-submit'])

const form = reactive({
  name: '',
  age: ''
})

const handleSubmit = () => {
  // 表单校验(简化)
  if (!form.name || !form.age) return alert('请填写完整信息')
  // 提交表单数据给父组件
  emit('form-submit', form)
  // 提交后重置表单
  form.name = ''
  form.age = ''
}
</script>

场景2:子组件关闭弹窗,父组件控制弹窗显示/隐藏

<!-- 子组件(ModalChild.vue) -->
<template>
  <div class="modal" v-if="visible">
    <div class="modal-content">
      <h4>子组件弹窗</h4>
      <button @click="handleClose">关闭弹窗</button>
    </div>
  </div>
</template>

<script setup>
const props = defineProps(['visible'])
const emit = defineEmits(['close-modal'])

const handleClose = () => {
  // 触发关闭事件,通知父组件隐藏弹窗
  emit('close-modal')
}
</script>

场景3:子组件列表删除,父组件更新列表

<!-- 子组件(ListChild.vue) -->
<template>
  <div class="list-child">
    <div v-for="item in list" :key="item.id">
      {{ item.name }}
      <button @click="handleDelete(item.id)">删除</button>
    </div>
  </div>
</template>

<script setup>
const props = defineProps(['list'])
const emit = defineEmits(['delete-item'])

const handleDelete = (id) => {
  // 传递要删除的id给父组件,由父组件更新列表
  emit('delete-item', id)
}
</script>

五、常见坑点避坑指南(新手必看)

很多新手在写子传父时,会遇到「父组件监听不到事件」「数据传递失败」等问题,以下是最常见的 4 个坑点,帮你快速避坑。

坑点1:事件名大小写不一致

子组件 emit('inputChange'),父组件 @inputchange="handle"(小写),会导致父组件监听不到事件。

解决方案:统一采用 kebab-case 命名,子组件 emit('input-change'),父组件 @input-change="handle"

坑点2:忘记声明事件(defineEmits)

子组件直接调用 emit('event'),未用 defineEmits 声明事件,虽然开发环境可能不报错,但生产环境可能出现异常,且 IDE 无提示。

解决方案:无论事件是否需要校验,都用 defineEmits 声明(数组形式即可)。

坑点3:传递复杂数据(对象/数组)时,父组件修改后影响子组件

子组件传递对象/数组给父组件,父组件直接修改该数据,会影响子组件(因为引用类型传递的是地址)。

解决方案:父组件接收数据后,用 JSON.parse(JSON.stringify(data)) 深拷贝,或用 reactive + toRaw 处理,避免直接修改原始数据。

坑点4:v-model 双向绑定时报错,提示「modelValue 未定义」

原因:子组件未接收 modelValue props,或未声明 update:modelValue 事件。

解决方案:确保子组件 defineProps(['modelValue'])defineEmits(['update:modelValue']) 都声明。

六、总结:子传父核心要点回顾

Vue3 子传父的核心就是「事件触发 + 事件监听」,记住以下 3 个核心要点,就能应对所有场景:

  1. 基础写法:defineEmits 声明事件 → emit 触发事件 → 父组件 @事件名 监听;
  2. 进阶优化:事件校验提升可靠性,v-model 简化双向绑定,遵循 kebab-case 命名规范;
  3. 避坑关键:事件名大小写一致、必声明事件、复杂数据深拷贝、v-model 对应 props 和 emit 命名正确。

子传父是 Vue3 组件通信中最基础的方式,掌握它之后,再学习父传子(props)、跨层级通信(provide/inject)、全局通信(Pinia)会更轻松。

沪指、创业板指午后均跌超1%

36氪获悉,沪指、创业板指午后均跌超1%,深成指跌0.97%;油气、有色金属跌幅居前,沪深京三市下跌个股超3000只。

高盛:预计从6月到今年底,会有四次降息

高盛全球投资级信贷主管Jonny Fine预计美联储今年将降息四次,第一次降息将在6月进行,随后的降息将分阶段进行,直至2026年结束。他将自己的鸽派立场归因于美联储领导层的更迭,并解释说,随着沃什担任美联储主席,他认为“美联储在制定货币政策决策时将采取更具前瞻性的立场”。(财联社)

蚂蚁集团开源万亿混合推理模型Ring-2.5-1T

36氪获悉,2月13日,蚂蚁集团开源发布全球首个基于混合线性架构的万亿参数思考模型 Ring-2.5-1T,在长文本生成、数学推理与智能体任务执行上达到开源领先水平。在生成效率上,Ring-2.5-1T在32K以上长文本生成场景中,对比上代模型访存规模降低10倍以上,生成吞吐提升3倍以上。

前端监控实践

从零开发前端监控 SDK:异常、性能、访问量一网打尽

本文将带你从零开发一个完整的前端监控 SDK,涵盖异常监控、性能监控和访问量统计三大核心功能。

目录

  1. 为什么需要前端监控
  2. SDK 架构设计
  3. 核心功能实现
  4. 使用示例
  5. 总结与展望

为什么需要前端监控

在现代 Web 应用中,前端监控已经成为保障用户体验的重要手段:

  • 异常监控:及时发现并修复线上 Bug,减少用户流失
  • 性能监控:优化页面加载速度,提升用户体验
  • 访问统计:了解用户行为,指导产品决策

市面上已有 Sentry、Fundebug 等成熟的监控服务,但开发自己的 SDK 能让我们:

  1. 完全掌控数据,保障隐私安全
  2. 根据业务需求定制功能
  3. 深入理解监控原理,提升技术能力

SDK 架构设计

整体架构

┌─────────────────────────────────────────────────────────────┐
│                        Monitor SDK                          │
├─────────────────────────────────────────────────────────────┤
│  Core Layer  │  Reporter (上报中心)  │  Config (配置管理)    │
├─────────────────────────────────────────────────────────────┤
│  Module Layer│  ErrorMonitor │ PerformanceMonitor │ VisitMonitor│
├─────────────────────────────────────────────────────────────┤
│  Utils Layer │  Device │ Storage │ UUID │ Sampling           │
└─────────────────────────────────────────────────────────────┘

设计原则

  1. 模块化:每个监控功能独立模块,可单独启用/禁用
  2. 插件化:Reporter 统一管理上报,支持批量和即时发送
  3. 低侵入:自动捕获异常,业务代码零改动
  4. 高兼容:支持多种引入方式(ESM/CJS/UMD)

核心功能实现

1. 异常监控模块

异常监控是 SDK 的核心功能,我们需要捕获多种类型的错误:

1.1 JavaScript 运行时错误
// src/modules/error/globalError.ts
export function initGlobalError(reporter: Reporter): () => void {
  const handler = (event: ErrorEvent) => {
    const errorData: ErrorData = {
      type: 'js',
      message: event.message,
      filename: event.filename,
      lineno: event.lineno,
      colno: event.colno,
      stack: event.error?.stack
    };
    reporter.report('error', errorData);
  };

  window.addEventListener('error', handler);
  return () => window.removeEventListener('error', handler);
}

通过监听 window.onerror,我们可以捕获所有同步和异步的 JavaScript 错误。

1.2 Promise 未捕获异常
// src/modules/error/promiseError.ts
export function initPromiseError(reporter: Reporter): () => void {
  const handler = (event: PromiseRejectionEvent) => {
    const errorData: ErrorData = {
      type: 'promise',
      message: event.reason?.message || String(event.reason),
      stack: event.reason?.stack
    };
    reporter.report('error', errorData);
  };

  window.addEventListener('unhandledrejection', handler);
  return () => window.removeEventListener('unhandledrejection', handler);
}

现代前端大量使用 Promise,未捕获的 Promise 错误会导致应用崩溃。

1.3 资源加载错误
// src/modules/error/resourceError.ts
export function initResourceError(reporter: Reporter): () => void {
  const handler = (event: Event) => {
    const target = event.target as HTMLElement;
    const tagName = target.tagName?.toLowerCase();

    if (!['img', 'script', 'link'].includes(tagName)) return;

    const src = (target as any).src || (target as any).href || '';
    const errorData: ErrorData = {
      type: 'resource',
      message: `Failed to load ${tagName}: ${src}`,
      filename: src,
      extra: { tagName }
    };
    reporter.report('error', errorData);
  };

  window.addEventListener('error', handler, true); // 捕获阶段监听
  return () => window.removeEventListener('error', handler, true);
}

使用捕获阶段(true)可以监听到资源加载错误。

1.4 网络请求错误

通过劫持 XMLHttpRequest 和 fetch API,监控所有网络请求:

// src/modules/error/networkError.ts
const originalFetch = window.fetch;
window.fetch = function(input: RequestInfo | URL, init?: RequestInit) {
  const startTime = Date.now();
  const url = typeof input === 'string' ? input : input.toString();

  return originalFetch.apply(this, arguments as any)
    .then(response => {
      if (!response.ok) {
        reporter.report('error', {
          type: 'network',
          message: `Fetch ${response.status}: ${response.statusText}`,
          extra: { method: init?.method || 'GET', url, status: response.status }
        });
      }
      return response;
    })
    .catch(error => {
      reporter.report('error', {
        type: 'network',
        message: `Fetch failed: ${error.message}`,
        extra: { method: init?.method || 'GET', url }
      });
      throw error;
    });
};

2. 性能监控模块

2.1 Web Vitals 指标

Core Web Vitals 是 Google 提出的衡量用户体验的关键指标:

// src/modules/performance/webVitals.ts

// LCP - 最大内容绘制
export function observeLCP(reporter: Reporter): void {
  const observer = new PerformanceObserver((list) => {
    const entries = list.getEntries();
    const lastEntry = entries[entries.length - 1];
    const value = (lastEntry as any).renderTime || lastEntry.startTime;

    reporter.report('performance', {
      type: 'web-vitals',
      name: 'LCP',
      value: Math.round(value),
      rating: value <= 2500 ? 'good' : value <= 4000 ? 'needs-improvement' : 'poor'
    });
  });

  observer.observe({ entryTypes: ['largest-contentful-paint'] as any });
}

// CLS - 累积布局偏移
export function observeCLS(reporter: Reporter): void {
  let clsValue = 0;

  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      const layoutEntry = entry as PerformanceEntry & { hadRecentInput: boolean; value: number };
      if (!layoutEntry.hadRecentInput) {
        clsValue += layoutEntry.value;
      }
    }
  });

  observer.observe({ entryTypes: ['layout-shift'] as any });

  // 页面隐藏时上报
  window.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') {
      reporter.report('performance', {
        type: 'web-vitals',
        name: 'CLS',
        value: Math.round(clsValue * 1000) / 1000,
        rating: clsValue <= 0.1 ? 'good' : clsValue <= 0.25 ? 'needs-improvement' : 'poor'
      });
    }
  });
}
2.2 导航性能

利用 Navigation Timing API 获取页面加载各阶段耗时:

export function observeNavigation(reporter: Reporter): void {
  window.addEventListener('load', () => {
    setTimeout(() => {
      const navigation = performance.getEntriesByType('navigation')[0]
        as PerformanceNavigationTiming;

      const metrics = [
        { name: 'DNS', value: navigation.domainLookupEnd - navigation.domainLookupStart },
        { name: 'TCP', value: navigation.connectEnd - navigation.connectStart },
        { name: 'TTFB', value: navigation.responseStart - navigation.startTime },
        { name: 'DOM解析', value: navigation.domInteractive - navigation.responseEnd },
        { name: 'Load', value: navigation.loadEventEnd - navigation.startTime }
      ];

      metrics.forEach(({ name, value }) => {
        if (value > 0) {
          reporter.report('performance', {
            type: 'navigation',
            name,
            value: Math.round(value)
          });
        }
      });
    }, 0);
  });
}
2.3 API 耗时监控

劫持 XMLHttpRequest 和 fetch,统计所有 API 请求耗时:

export function observeAPI(reporter: Reporter): () => void {
  // 劫持 XMLHttpRequest
  const originalXHRSend = XMLHttpRequest.prototype.send;

  XMLHttpRequest.prototype.send = function() {
    const startTime = Date.now();

    this.addEventListener('loadend', function() {
      const duration = Date.now() - startTime;
      reporter.report('performance', {
        type: 'api',
        name: `API: ${this._url}`,
        value: duration
      });
    });

    return originalXHRSend.apply(this, arguments);
  };

  // 劫持 fetch...
}

3. 访问监控模块

3.1 PV 统计
// src/modules/visit/pv.ts
export function observePV(reporter: Reporter, enableSPA: boolean): () => void {
  // 初始页面 PV
  reportPV(reporter);

  if (!enableSPA) return;

  // 劫持 history API 监听路由变化
  const originalPushState = history.pushState;
  history.pushState = function(...args) {
    originalPushState.apply(this, args);
    reportPV(reporter);
  };

  window.addEventListener('popstate', () => reportPV(reporter));
  window.addEventListener('hashchange', () => reportPV(reporter));
}
3.2 Session 管理
// src/modules/visit/session.ts
const SESSION_TIMEOUT = 30 * 60 * 1000; // 30分钟

export function initSession(reporter: Reporter): void {
  const startTime = Date.now();

  // 上报会话开始
  reporter.report('visit', { type: 'session-start' });

  // 页面可见性变化
  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'visible') {
      const lastActive = parseInt(storage.get('session_time') || '0');
      if (Date.now() - lastActive > SESSION_TIMEOUT) {
        // 新会话
        reporter.report('visit', { type: 'session-start' });
      }
    } else {
      storage.set('session_time', Date.now().toString());
    }
  });

  // 页面卸载时上报会话结束
  window.addEventListener('beforeunload', () => {
    reporter.report('visit', {
      type: 'session-end',
      duration: Date.now() - startTime
    });
  });
}

4. 数据上报中心

4.1 上报策略
// src/core/reporter.ts
export class Reporter {
  private queue: QueueItem[] = [];
  private readonly FLUSH_INTERVAL = 5000; // 5秒刷新
  private readonly MAX_QUEUE_SIZE = 10;   // 10条批量发送

  report(type: ReportData['type'], data: ReportData['data']): void {
    // 采样检查
    const sampleRate = this.config.sampleRate?.[type] || 1;
    if (!shouldSample(sampleRate)) return;

    const url = this.config.reportUrl[type];
    if (!url) return;

    // 异常数据立即上报
    if (type === 'error') {
      this.sendImmediately(data, url);
    } else {
      // 性能和访问数据批量上报
      this.addToQueue(data, url);
    }
  }

  private addToQueue(data: ReportData, url: string): void {
    this.queue.push({ data, url });

    if (this.queue.length >= this.MAX_QUEUE_SIZE) {
      this.flush();
    } else {
      this.scheduleFlush();
    }
  }
}
4.2 页面关闭补发

使用 sendBeacon API 在页面关闭前发送剩余数据:

private bindEvents(): void {
  const sendRemaining = () => {
    if (this.queue.length === 0) return;

    this.queue.forEach(({ data, url }) => {
      navigator.sendBeacon?.(url, JSON.stringify(data));
    });
  };

  window.addEventListener('beforeunload', sendRemaining);
  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') {
      sendRemaining();
    }
  });
}

5. 设备信息解析

// src/utils/device.ts
export function getDeviceInfo(): DeviceInfo {
  const ua = navigator.userAgent;

  // 解析操作系统
  let os = 'unknown';
  let osVersion = 'unknown';

  if (ua.indexOf('Win') !== -1) {
    os = 'Windows';
    const match = ua.match(/Windows NT (\d+\.\d+)/);
    if (match) osVersion = match[1];
  } else if (ua.indexOf('Mac') !== -1) {
    os = 'macOS';
    // ...
  } else if (/iPad|iPhone|iPod/.test(ua)) {
    os = 'iOS';
    // ...
  } else if (ua.indexOf('Android') !== -1) {
    os = 'Android';
    // ...
  }

  // 解析浏览器
  let browser = 'unknown';
  let browserVersion = 'unknown';

  if (ua.indexOf('Chrome') !== -1 && ua.indexOf('Edg') === -1) {
    browser = 'Chrome';
    const match = ua.match(/Chrome\/(\d+\.\d+)/);
    if (match) browserVersion = match[1];
  }
  // ... Safari, Firefox, Edge

  return {
    ua,
    os,
    osVersion,
    browser,
    browserVersion,
    screen: `${window.screen.width}x${window.screen.height}`,
    language: navigator.language
  };
}

使用示例

基础使用

import Monitor from 'frontend-monitor-sdk';

Monitor.init({
  appId: 'my-app',
  appVersion: '1.0.0',
  env: 'production',
  reportUrl: {
    error: 'https://api.example.com/error',
    performance: 'https://api.example.com/perf',
    visit: 'https://api.example.com/visit'
  },
  sampleRate: {
    error: 1,         // 异常100%上报
    performance: 0.1, // 性能10%采样
    visit: 0.1        // 访问10%采样
  },
  enableSPA: true,
  beforeReport: (data) => {
    // 上报前钩子,可修改数据或返回 false 阻止上报
    if (data.type === 'error' && data.data.message?.includes('ignore')) {
      return false;
    }
    return data;
  }
});

Vue 集成

import { createApp } from 'vue';
import Monitor from 'frontend-monitor-sdk';

Monitor.init({ /* ... */ });

const app = createApp(App);
app.config.errorHandler = Monitor.vueErrorHandler;

React 集成

class ErrorBoundary extends React.Component {
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    Monitor.reportError(error, {
      componentStack: errorInfo.componentStack
    });
  }

  render() {
    return this.props.children;
  }
}

总结与展望

已实现功能

异常监控:JS 错误、Promise 错误、资源错误、网络错误、控制台错误、框架错误 ✅ 性能监控:Web Vitals、导航计时、资源性能、API 耗时、长任务 ✅ 访问监控:PV/UV、Session、设备信息、SPA 路由监听 ✅ 数据上报:分类上报、采样控制、批量上报、页面关闭补发

技术亮点

  1. 类型安全:完整的 TypeScript 类型定义
  2. 模块化设计:各功能独立,可灵活组合
  3. 低侵入性:自动捕获,业务代码零改动
  4. 高兼容性:支持 ESM/CJS/UMD 多种格式

未来优化方向

🔲 SourceMap 解析:实现错误堆栈的源码还原 🔲 用户行为录屏:记录用户操作路径,辅助问题定位 🔲 性能面板可视化:开发 Chrome 插件查看性能数据 🔲 离线缓存:支持网络断开时的数据本地存储


参考资源


本文完,如有问题欢迎留言讨论!

华为原终端BG多媒体技术部部长邓某因涉嫌非国家工作人员受贿罪被逮捕

据华为内部反腐快报2026(001)文件显示,原终端BG多媒体技术部部长邓某因涉嫌非国家工作人员受贿罪被龙岗区人民检察院依法批准逮捕。犯罪嫌疑人邓某,于2007年7月入职华为,2007年7月至2020年12月先后任职终端总体技术硬件平台部工程师、终端器件与归一化部高级工程师、终端多媒体技术部高级工程师、架构师、Camera部经理、多媒体技术部部长等职务。(界面)

紫金矿业注册资本增至约26.6亿元

36氪获悉,爱企查App显示,近日,紫金矿业集团股份有限公司发生工商变更,注册资本由约26.3亿元人民币增至约26.6亿元人民币。紫金矿业集团股份有限公司成立于2000年9月,法定代表人为邹来昌,经营范围包括矿产资源勘查、金矿采选、金冶炼、铜矿采选、信息系统集成服务、信息技术咨询服务等。股东信息显示,该公司由闽西兴杭国有资产投资经营有限公司、香港中央结算(代理人)有限公司、香港中央结算有限公司等共同持股。

锂业分会:1月碳酸锂价格大幅上涨,市场交易活跃

36氪获悉,中国有色金属工业协会锂业分会发布2026年1月锂行业运行情况。1月,碳酸锂价格大幅上涨,市场交易活跃,基差拉大。供应端,碳酸锂生产平稳,部分正极材料企业产线检修。需求端,国补延续提升新能源车市场预期,锂电池出口退税下降提振一季度抢出口需求。

融资丨蔚能完成10亿元C3轮融资

2026年2月13日,蔚能宣布完成C3轮股权融资,本轮融资金额为10亿元,截至目前蔚能C轮累计融资金额达近20亿元。在创始股东追加投资,引入海宁经开、海南澄迈、眉山东坡数家国有资本股东的基础上,公司在C3轮融资中进一步引进合肥建投、合肥经开两家重要股东,新老股东的加持和战略资本的不断引入,代表着投资人对蔚能商业模式、财务表现和未来发展前景的持续看好,为公司电池资产管理业务拓展、电池技术创新研发等,提供了更为雄厚的资源和资金保障。

截至目前,蔚能在运营电池资产规模已突破42GWh,服务用户总数量已超过55万人,公司以动力电池全周期数智化管理助力新能源电动汽车产业发展,推动产业化成果落地。蔚能成立五年多深耕电池资产管理领域,实现电池应用技术的深度积累,并荣获“商务部全国技术贸易创新实践案例”、“湖北省科学技术进步奖一等奖”等多项权威奖项认证。截止到2025年底,公司已申请专利196项,其中发明占比约60%,电池技术占比85%以上。

本次国资背景新股东的战略引入,是公司发展得到资本市场认可和支持的有力证明,融资资金将主要用于公司电池资产管理相关业务的投放和技术研发、资源循环等业务领域,助力蔚能电池资产管理服务水平的持续提升,增强公司研发和资金实力,加速科研成果的产业化落地。

蔚能也将紧密携手投资方等合作伙伴,让电池资产管理服务更加广泛地惠及千家万户,推动新能源行业高质量发展。

查看更多项目信息,请前往「睿兽分析」。

元气森林唐彬森发内部信:业绩已连续三年保持两位数增长

2月13日,元气森林创始人唐彬森发布九周年内部信,他在信中指出,2025年公司盈利能力持续提升,增长质量与效率较往年明显改善。2024年公司初步验证盈利可能性,2025年则在高质量增长上实现关键突破。据了解,元气森林整体业绩已连续三年保持两位数增长。从具体品类表现来看,维生素水成为增速最高的单品,2025年同比增长128%;冰茶系列次之,增长56%。好自在系列与外星人电解质水分别增长36%和34%。(界面)
❌