普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月28日首页

Sentry 错误监控系统

2026年1月28日 14:30

Vue2 项目从零搭建免费 Sentry 错误监控系统完整指南

老 Vue2 项目搭建免费的 Sentry 错误监控系统,从注册账号到最终使用的全部步骤。


第一部分:注册 Sentry 账号

1. 访问 Sentry 官网

打开浏览器,访问 sentry.io/signup/

2. 注册账号

有以下几种注册方式:

  • GitHub 账号登录(推荐,最快捷)
  • Google 账号登录
  • 邮箱注册

💡 免费套餐说明:Sentry 免费版每月支持 5,000 个错误事件,对于个人项目或小型团队完全够用。

3. 完成注册流程

  1. 选择登录/注册方式
  2. 填写组织名称(Organization Name),例如:my-company
  3. 确认邮箱(如果是邮箱注册)

第二部分:创建 Sentry 项目并获取 DSN

1. 创建新项目

登录后,进入 Sentry 控制台:

  1. 点击左侧菜单 "Projects"
  2. 点击右上角 "Create Project" 按钮
  3. 在平台列表中选择 "Vue"
  4. 设置告警规则(Alert Settings):
    • 选择 "Alert me on every new issue"(推荐新手)
    • 或者选择自定义规则
  5. 输入项目名称,例如:my-vue2-app
  6. 选择团队(如果有多个团队)
  7. 点击 "Create Project"

2. 获取 DSN(数据源名称)

项目创建成功后,系统会自动跳转到配置页面,显示你的 DSN。

如果错过了这个页面,可以通过以下方式找到 DSN:

  1. 进入项目 → Settings(设置)
  2. 左侧菜单选择 Client Keys (DSN)
  3. 复制 DSN,格式类似:
    https://xxxxxxxxxxxxxxxx@o123456.ingest.sentry.io/1234567
    

⚠️ 重要:DSN 是你项目的唯一标识,请妥善保管,不要泄露到公开的代码仓库中。


第三部分:在 Vue2 项目中安装 Sentry SDK

1. 安装依赖包

在你的 Vue2 项目根目录下执行:

# 使用 npm
npm install @sentry/vue@5 @sentry/tracing@5 --save

# 或使用 yarn
yarn add @sentry/vue@5 @sentry/tracing@5

⚠️ Vue2 必须使用 @sentry/vue@5 版本,@sentry/vue@7+ 版本只支持 Vue3。

2. 确认安装成功

查看 package.json,确保依赖已添加:

{
  "dependencies": {
    "@sentry/vue": "^5.x.x",
    "@sentry/tracing": "^5.x.x"
  }
}

第四部分:配置 Sentry 初始化代码

1. 修改入口文件 src/main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router' // 如果你使用了 vue-router

// 引入 Sentry
import * as Sentry from '@sentry/vue'
import { Integrations } from '@sentry/tracing'

// 初始化 Sentry(必须在 new Vue() 之前)
Sentry.init({
  Vue,
  dsn: 'https://你的DSN@o123456.ingest.sentry.io/1234567', // 替换为你的 DSN
  integrations: [
    new Integrations.BrowserTracing({
      routingInstrumentation: Sentry.vueRouterInstrumentation(router),
      tracingOrigins: ['localhost', 'your-domain.com', /^\//]
    })
  ],
  // 性能监控采样率(1.0 = 100%,生产环境建议设置 0.1-0.2)
  tracesSampleRate: 1.0,
  // 环境标识
  environment: process.env.NODE_ENV || 'development',
  // 在控制台也输出错误(开发时有用)
  logErrors: true
})

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

2. 如果没有使用 vue-router 的简化配置

import Vue from 'vue'
import App from './App.vue'
import * as Sentry from '@sentry/vue'

Sentry.init({
  Vue,
  dsn: 'https://你的DSN@o123456.ingest.sentry.io/1234567',
  tracesSampleRate: 1.0,
  environment: process.env.NODE_ENV || 'development',
  logErrors: true
})

new Vue({
  render: h => h(App)
}).$mount('#app')

第五部分:使用环境变量管理 DSN(推荐)

为了安全起见,建议使用环境变量管理 DSN。

1. 创建环境变量文件

在项目根目录创建 .env 文件:

# .env.development(开发环境)
VUE_APP_SENTRY_DSN=https://开发环境的DSN

# .env.production(生产环境)
VUE_APP_SENTRY_DSN=https://生产环境的DSN

2. 修改 main.js 使用环境变量

Sentry.init({
  Vue,
  dsn: process.env.VUE_APP_SENTRY_DSN,
  // ... 其他配置
})

3. 将 .env 文件添加到 .gitignore

# .gitignore
.env
.env.local
.env.*.local

第六部分:测试 Sentry 是否正常工作

1. 添加测试按钮

在任意组件中添加一个测试按钮,例如 App.vue

<template>
  <div id="app">
    <h1>Sentry 测试</h1>
    <button @click="throwTestError">触发测试错误</button>
    <button @click="throwTypeError">触发类型错误</button>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    throwTestError() {
      throw new Error('这是一个 Sentry 测试错误!')
    },
    throwTypeError() {
      // 故意访问未定义变量的属性
      const obj = undefined
      console.log(obj.property)
    }
  }
}
</script>

2. 运行项目并测试

npm run serve
  1. 打开浏览器访问项目
  2. 点击测试按钮触发错误
  3. 打开浏览器开发者工具(F12),查看 Network 标签
  4. 你应该能看到发送到 sentry.io 的请求

3. 在 Sentry 控制台查看错误

  1. 登录 sentry.io
  2. 进入你的项目
  3. 点击左侧 "Issues" 菜单
  4. 你应该能看到刚才触发的测试错误

💡 错误通常会在几秒钟内出现在 Sentry 控制台中。


第七部分:高级配置(可选但推荐)

1. 捕获 API 请求错误

如果你使用 axios,可以添加拦截器:

// src/utils/request.js
import axios from 'axios'
import * as Sentry from '@sentry/vue'

const service = axios.create({
  baseURL: process.env.VUE_APP_API_URL,
  timeout: 10000
})

// 响应拦截器
service.interceptors.response.use(
  response => response,
  error => {
    // 将 API 错误发送到 Sentry
    Sentry.captureException(error)
    
    // 添加额外上下文信息
    Sentry.setContext('api_error', {
      url: error.config?.url,
      method: error.config?.method,
      status: error.response?.status,
      data: error.response?.data
    })
    
    return Promise.reject(error)
  }
)

export default service

2. 添加用户信息(用于追踪特定用户的问题)

// 用户登录成功后调用
import * as Sentry from '@sentry/vue'

function onUserLogin(user) {
  Sentry.setUser({
    id: user.id,
    username: user.username,
    email: user.email // 可选,注意隐私保护
  })
}

// 用户登出时清除
function onUserLogout() {
  Sentry.setUser(null)
}

3. 使用 Vue 错误边界捕获组件错误

// 在 main.js 中添加全局错误处理
Vue.config.errorHandler = (err, vm, info) => {
  Sentry.captureException(err, {
    extra: {
      componentName: vm?.$options?.name,
      propsData: vm?.$options?.propsData,
      lifecycleHook: info
    }
  })
}

4. 手动捕获错误和消息

import * as Sentry from '@sentry/vue'

// 捕获异常
try {
  riskyOperation()
} catch (error) {
  Sentry.captureException(error)
}

// 发送自定义消息
Sentry.captureMessage('用户完成了重要操作', 'info')

// 添加面包屑(用于追踪用户操作路径)
Sentry.addBreadcrumb({
  category: 'user-action',
  message: '用户点击了购买按钮',
  level: 'info'
})

5. 配置不同环境的采样率

// src/config/sentry.js
export const sentryConfig = {
  development: {
    tracesSampleRate: 1.0,  // 开发环境 100% 采样
    debug: true
  },
  production: {
    tracesSampleRate: 0.2,  // 生产环境 20% 采样(节省配额)
    debug: false
  }
}

第八部分:Source Maps 配置(用于定位压缩代码)

生产环境的代码经过压缩后,错误堆栈很难阅读。上传 Source Maps 可以让 Sentry 显示原始代码位置。

1. 安装 Sentry CLI

npm install @sentry/cli --save-dev

2. 创建 .sentryclirc 配置文件

在项目根目录创建 .sentryclirc

[defaults]
url = https://sentry.io
org = 你的组织名
project = 你的项目名

[auth]
token = 你的AuthToken

3. 获取 Auth Token

  1. 登录 Sentry
  2. 进入 SettingsAccountAPIAuth Tokens
  3. 点击 Create New Token
  4. 勾选 project:releasesorg:read 权限
  5. 复制生成的 Token

4. 在构建时上传 Source Maps

修改 package.json

{
  "scripts": {
    "build": "vue-cli-service build",
    "postbuild": "sentry-cli releases files $npm_package_version upload-sourcemaps ./dist/js --url-prefix '~/js'"
  }
}

第九部分:Sentry 控制台功能介绍

Issues(问题列表)

  • 查看所有捕获的错误
  • 按频率、影响用户数排序
  • 标记问题状态(已解决/忽略)

Performance(性能监控)

  • 页面加载时间
  • API 请求耗时
  • 性能瓶颈分析

Alerts(告警设置)

  • 设置错误阈值告警
  • 配置邮件/Slack/钉钉通知

Releases(版本管理)

  • 关联代码版本
  • 追踪哪个版本引入了问题

第十部分:常见问题排查

问题 解决方案
控制台没有收到错误 检查 DSN 是否正确;确保网络能访问 sentry.io
错误信息不完整 确认 SDK 版本正确(Vue2 用 v5)
Source Map 不生效 检查 release 版本是否匹配;确认上传成功
免费额度用完 降低 tracesSampleRate 采样率;或升级套餐

完整配置示例

最终的 src/main.js 完整代码:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import * as Sentry from '@sentry/vue'
import { Integrations } from '@sentry/tracing'

// 仅在有 DSN 时初始化 Sentry
if (process.env.VUE_APP_SENTRY_DSN) {
  Sentry.init({
    Vue,
    dsn: process.env.VUE_APP_SENTRY_DSN,
    integrations: [
      new Integrations.BrowserTracing({
        routingInstrumentation: Sentry.vueRouterInstrumentation(router),
        tracingOrigins: ['localhost', /^\//]
      })
    ],
    tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.2 : 1.0,
    environment: process.env.NODE_ENV,
    release: process.env.VUE_APP_VERSION || '1.0.0',
    logErrors: process.env.NODE_ENV === 'development'
  })
}

// 全局错误处理
Vue.config.errorHandler = (err, vm, info) => {
  console.error('Vue Error:', err)
  Sentry.captureException(err, {
    extra: { componentName: vm?.$options?.name, info }
  })
}

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

总结

完成以上步骤后,你的 Vue2 项目就已经成功接入了 Sentry 错误监控系统。当用户在使用你的应用时遇到错误,你可以:

  1. 实时收到错误通知
  2. 查看完整的错误堆栈和上下文
  3. 了解错误影响的用户数量
  4. 追踪用户操作路径(面包屑)
  5. 关联代码版本定位问题

阿斯麦官宣120亿欧元股票回购计划,2025年净利润96亿欧元

2026年1月28日 14:28
1月28日,阿斯麦公布其2025年第四季度及全年业绩。该公司2025年总净销售额为327亿欧元,净利润为96亿欧元。阿斯麦预计2026年总净销售额将在340亿至390亿欧元之间,毛利率在51%至53%之间。此外,阿斯麦宣布一项新的股票回购计划,总额至多120亿欧元,将于2028年12月31日前执行。(界面)

工信部:截至2025年底,我国移动电话用户规模达18.27亿户

2026年1月28日 14:25
36氪获悉,工信部发布2025年通信业统计公报,截至2025年底,我国移动电话用户规模达18.27亿户。移动电话用户普及率达130部/百人,较全球平均水平(107.5部/百人)高出22.5个点。移动电话用户快速向5G迁移,5G移动电话用户数达12.04亿户,本年净增达1.9亿户,在移动电话用户中占比65.9%,达全球平均水平2.1倍,占比较上年末提高9.2个百分点。

工信部:截至2025年底,我国5G基站数达483.8万个

2026年1月28日 14:24
36氪获悉,工信部发布2025年通信业统计公报,截至2025年底,我国5G基站数达483.8万个,占移动电话基站数比重达37.6%,平均每万人拥有5G基站34.4个,高于“十四五”规划发展主要目标8.4个;千兆网络建设深入推进,具备千兆网络服务能力的10GPON端口数达3162万个,达“十四五”规划发展主要目标的2.6倍。加快推动5G-A、万兆网络试点部署,5GRedCap基站数达206.4万个,5G-A覆盖超330个城市,首批168个小区、工厂和园区的万兆光网试点部署顺利开展。

数据工程决策:自研 vs 采购 NoETL 自动化指标平台的深度分析

2026年1月28日 14:21

本文首发于 Aloudata 官方技术博客:《自研指标平台是大坑?80%企业选择采购NoETL自动化指标平台》转载请注明出处。

摘要:本文深入剖析了企业自研指标平台面临的三大核心技术挑战:统一语义层构建、智能物化加速与开放生态适配。通过对比传统静态指标字典与 NoETL 动态语义引擎的架构差异,并结合总拥有成本(TCO)分析,论证了对于绝大多数企业而言,采购成熟的 NoETL 自动化指标平台是实现数据敏捷、降低长期成本、规避技术风险的理性选择。

认知误区:你以为在做“字典”,实际需要打造“引擎”

企业启动自研指标平台的初衷通常是解决“口径乱”的问题,希望建立一个统一的指标目录或“字典”。然而,在 AI 驱动的数智化运营时代,业务对数据的灵活性要求呈指数级增长。一个简单的指标目录,无法支撑业务人员“任意维度、任意筛选”的自助分析,更无法让 AI 智能体(Agent)理解并调用。

“传统 ETL 通过宽表和汇总表交付指标的模式,导致了大量指标的重复开发,造成企业在存储和计算上的巨大浪费。” —— Aloudata CAN 产品白皮书

问题的本质在于,支撑现代数据分析的并非一个静态的“字典”,而是一个能实时工作的“引擎”。这个引擎需要具备:

  • 语义解析能力:理解业务术语(如“有效销售额”)背后的复杂计算逻辑(SUM(订单金额) - SUM(退款金额))。
  • 动态计算能力:在不预建物理宽表的前提下,实时关联多张明细表,生成“虚拟业务事实网络”。
  • 性能保障能力:通过智能化的物化加速,确保对海量明细数据的查询也能获得秒级响应。

自研项目往往始于对“统一口径”的朴素追求,却最终陷入构建一个企业级“语义计算引擎”的深水区,其技术复杂度和资源需求远超初期规划。

自研模式.png

挑战一:语义解析——从“静态表”到“动态虚拟宽表”

这是自研面临的第一道技术鸿沟。传统方式通过 ETL 工程师编写 SQL,将业务逻辑固化在物理宽表(DWS/ADS)中。而 NoETL 语义编织要求平台构建一个“统一语义层”,在不进行物理打宽的前提下,通过声明式建模,让系统能实时理解并关联跨多张明细表(DWD)的业务逻辑,形成“虚拟业务事实网络”。

这要求自研团队具备编译原理、查询优化和复杂业务抽象能力,而非简单的 SQL 封装。具体挑战包括:

  1. 逻辑关联的动态解析:如何让系统理解“订单表”的“客户 ID”与“客户维度表”的“客户 ID”在业务上等价,并能处理多通路等复杂场景。这需要设计一套元数据模型来声明和管理表间关联关系。
  2. 复杂指标的函数化封装:如何将“近 30 天消费金额 >5000 的客户数”这类业务需求,配置化为可复用的语义函数(如跨表限定、指标维度化、二次聚合),而无需为每个需求手写数百行 SQL。这本质上是构建一个面向业务人员的“高级查询语言”及其编译器。
  3. NL2Metrics 的意图理解:若想对接 AI,还需构建让大模型能理解的“语义知识图谱”,实现从自然语言到指标调用的精准转换(NL2Metrics),从根源上根治数据幻觉。这需要将业务指标、维度、限定条件等语义元数据结构化,并提供标准的 Function Calling 接口。

自研团队需要从“SQL 脚本执行者”转变为“语义编译器设计者”,这是一个质的飞跃。

挑战二:智能物化——从“人工运维”到“系统自治”

即使解决了语义解析,面对企业百亿级的明细数据,如何保障查询的秒级响应?传统做法是数据工程师基于经验,手动创建和维护大量的物化视图(加速表)。但这种方式成本高昂、响应滞后,且极易形成新的数据冗余。

NoETL 平台的智能物化加速,其核心并非取消 ETL,而是将其升级为一种由“声明式策略”驱动的自动化性能服务。自研实现这一能力的难点在于:

  1. 物化策略的自动生成与优化:如何基于用户对指标和维度的“加速声明”,结合数据分布和查询历史,自动设计出存储成本与查询性能最优的物化方案,并支持去重计数、比率类等复杂指标的上卷。
  2. 查询的透明改写与路由:如何让用户的查询请求(无论是来自 BI 拖拽还是 AI 调用)无感知地自动路由到最优的物化结果上,并完成底层 SQL 的透明改写,这对查询优化器的要求极高。
  3. 口径变更影响的全面分析:如何在指标口径变更时自动识别并提示所有下游影响,辅助用户根据变更影响告警进行物化任务重建和数据回刷操作,这对数据血缘解析有着极高的要求。

“通过智能物化加速确保十亿、百亿级明细数据的秒级查询响应。” —— NoETL 指标平台白皮书

这要求自研团队不仅精通数据库内核优化,还需具备平台级的资源调度与成本管控(FinOps)能力。

挑战三:生态适配——从“孤岛工具”到“中立基座”

指标平台的终极价值在于被消费。企业内往往存在多种 BI 工具(如 Tableau、Power BI)、业务系统和新兴的 AI 应用。自研平台极易陷入为某个特定前端(如某个自研报表系统)深度定制的陷阱,成为一个新的“数据孤岛”。

真正的指标平台必须是中立的“数据中枢”,其挑战在于:

  1. 标准化接口设计与实现:提供稳定、高性能的 Restful API 和成熟的 JDBC 驱动,确保下游各类应用能无差别、高性能地调用指标服务。
  2. 治理规则的内嵌与强制执行:将企业的数据安全策略(行列级权限)、审批流程等治理要求,平台化、内嵌化到指标的生产和消费链路中,从技术上保障“One Truth”的落地,而非依赖人工监督。
  3. 与现有数据湖仓的平滑集成:无需推翻重来,能通过标准连接器对接企业已有的各类数据湖仓,实现对存量宽表的“挂载”与新需求的“原生”建模混合策略,保护既有投资。

生态适配能力决定了平台是企业长期演进的“基石”还是又一个短命的“项目”。

TCO分析:自研与采购的总拥有成本对比

决策必须超越初始采购费用,基于总拥有成本(TCO)进行理性分析。自研的初始开发成本只是冰山一角,后续高昂的持续维护、升级、扩容成本,以及因效率低下导致的业务机会成本,构成了“隐形高利贷”。

成本维度 自研模式 (典型问题) 采购 NoETL 平台 (典型收益)
人力成本 组建并长期供养一支精通数据架构、编译原理、分布式系统的顶尖团队,招聘难、流失风险高。 将数据工程师从重复 ETL 开发中解放,转向高价值的语义建模与业务赋能,人力结构优化。
开发与运维成本 语义解析能力、动态查询能力、只能物化能力、查询命中与上卷等复杂功能需人工持续设计、开发、调试、运维,复杂度线性攀升。 成熟平台实现自动化指标生产、智能物化与查询路由,运维复杂度大幅降低,实现“以销定产”。
机会成本 需求响应慢(周/天级),压抑业务探索,错失市场机会;数据口径混乱,引发决策风险。传统方案探索性分析准确率仅 40%。 需求分钟级响应,激活业务自助分析;口径 100% 一致,构建决策信任基石。复杂任务准确率可达 98.75%。

根据第三方测试数据,采用成熟的 NoETL 架构平台,可实现 3 年 TCO 降低 45%,需求平均响应时间缩短 90.71%,从“成本中心”转变为“效率引擎”。(来源:相关技术评测报告)

NoETL 模式.png

决策矩阵:何时该自研,何时该果断采购?

企业不应一概而论。通过以下决策矩阵,可以清晰判断自身情况:

应果断采购,若:

  • 核心目标是快速实现业务数据化运营与敏捷决策。
  • 缺乏构建并长期维护复杂数据计算引擎(语义引擎、智能物化)的核心技术团队。
  • 需要对接多种 BI 工具和 AI 应用,避免厂商锁定。
  • 希望控制长期 TCO,避免技术债务失控,追求确定性回报。

可谨慎评估自研,若:

  • 拥有极其特殊、封闭且稳定的业务场景,市面产品完全无法满足。
  • 具备世界级的数据系统工程团队,且将自研平台作为核心战略产品投入。
  • 不计较时间与金钱成本,旨在技术积累。

对于绝大多数追求数据敏捷、希望快速获得业务价值的企业,采购成熟的 NoETL 自动化指标平台是明确的最优解。

常见问题 (FAQ)

Q1: 自研指标平台,初期投入大概需要多少人和多长时间?

初期投入严重低估是常见陷阱。要打造一个具备基本语义解析和查询能力的原型,至少需要一个 5-8 人的资深团队(含架构、前后端、数据开发),耗时 6-12 个月。而这仅能达到“可用”水平,距离支撑企业级复杂分析、智能物化和 AI 对接的“好用”阶段,还需持续投入 2-3 年及更多资源进行迭代和运维,总成本远超预期。

Q2: 采购 NoETL 指标平台,如何与我们现有的数据仓库集成?

成熟的 NoETL 平台设计为中立的数据基座。它通过标准连接器直接读取您现有数据仓库的公共明细层(DWD)数据,无需数据搬迁。平台在逻辑层构建语义模型和虚拟宽表,对下游提供统一 API 服务。现有 BI 报表和 ETL 任务可以逐步迁移至新平台消费,实现平滑演进,保护既有投资。

Q3: 如果未来业务变化很大,采购的平台会不会不够灵活?

这正是 NoETL 平台的核心优势——应对变化。其“语义模型驱动”的架构,将易变的业务逻辑(指标口径、维度关联)上浮至可配置的语义层,而将稳定的物理存储与计算下放。当业务变化时,只需在语义层修改或新增配置,无需改动底层 ETL 和物理表。这种解耦设计使平台天生具备极强的业务适应性。

Q4: 如何验证平台真能解决“口径不一致”和“响应慢”的问题?

要求在 POC(概念验证)中设置真实业务场景:1) 口径验证:在平台中统一定义一个核心指标(如“有效销售额”),并确保通过 API 在不同测试报表中调用结果完全一致。2) 性能验证:针对一个涉及多表关联和复杂筛选的灵活分析需求,测试从发起查询到获取结果的端到端响应时间,要求达到秒级。同时,核查厂商提供的同类客户案例中的量化收益数据。

核心要点

  1. 本质是引擎,而非字典:自研指标平台的核心挑战是构建具备实时语义解析与智能物化能力的“动态计算引擎”,技术复杂度远超一个静态的指标目录。
  2. 三大挑战难以逾越:语义解析(构建虚拟宽表)、智能物化(系统自治的性能服务)、生态适配(中立的数据中枢)是自研工程实现上的核心难点,需要顶尖的架构与工程团队。
  3. TCO 揭示真实成本:自研的隐性成本(长期维护、机会成本)极高,而采购成熟平台能获得开发提效 10 倍、存算成本降 70%、分钟级响应的确定性回报。
  4. 采购是理性决策:对于绝大多数追求数据敏捷与业务价值的企业,采购经过大规模复杂场景验证的 NoETL 自动化指标平台,是规避风险、加速见效的最优路径。

本文首发于 Aloudata 官方技术博客,查看更多技术细节与高清架构图,请访问原文链接:ai.noetl.cn/knowledge_b…

【flex学习】图解flex的三个属性:grow、shrink、basis

作者 熬夜仙人
2026年1月28日 14:15

最近在准备年后的面试,趁现在有时间把css部分好好补一下,就先从flex的三个属性开始吧。

前置知识:了解flex的基本使用,了解盒模型。

flex-grow

默认值:0

作用:当元素无法充满父元素时,定义剩余的空间怎么分配(添加到子元素上)。

使用方法如图所示

image.png

image.png

image.png

image.png 源码:

flex-shrink

默认值:1

作用:与grow相反,shrink用于定义在父元素宽度不够的情况下,子元素如何分配空间,shrink越大,代表需要承担的越多。

计算方法

  1. 先计算出溢出的宽度
  2. 计算出子元素各自的shrink比例(个体÷所有)
  3. 计算出每块应该承担的宽度(溢出的宽度*每一块所占比例),在每一块中删除

例子

前提:父元素宽度为120,gap为10,因此实际可以放元素的区域为110; 子元素width都为50。

1. shrink为默认值1

则每一个子元素的实际宽度都相同,都为:50 - (3*50 - 110) * (50 / 150)≈ 36.66

2. 第一个的shrink为0.5,其他为默认值1

则计算方法为:50 - (150 - 110) * (0.5 / 2.5) = 42

但实际上并不是42,而是43.6,这是为什么?

因为此处的子元素有边框,而盒模型使用的是标准盒模型。width不包括borderpadding,实际占用的宽度为50 + 2 * 2 = 54,计算的结果为:54 - (543 - 110) * [540.5 / (54*2.5)] = 43.6

shrink为1时,浏览器为了简化计算,忽略了border直接取width值。

3. 第一个shrink为0.5,第二个shrink为0.3,第三个shrink为0.2

则计算方法为:
第一个:54 - (543 - 110) * [(540.5) / (540.5 + 540.3 + 54*0.2)] = 28

第二个:54 - (543 - 110) * [(540.3) / (540.5 + 540.3 + 54*0.2)] = 38.4

第三个:54 - (543 - 110) * [(540.2 / (540.5 + 540.3 + 54*0.2)] = 43.6

源码:

flex-basis

此处参考了张鑫旭老师的文章,有时间的同学推荐阅读一下,还有很多细节的东西我没有写:www.zhangxinxu.com/wordpress/2…

默认值:auto

作用:设定元素在flex布局下的基础尺寸,可以完全替代width。当basisauto时,才会去取width的值。

注意

  1. 当内容超出basis的范围时,元素尺寸会自动拉伸。但此时如果存在width,元素尺寸会以width为准。因此尽量不要同时使用width和basis。

  1. 关于flex布局中尺寸的生效优先级:min-width/max-width > basis > width

结语

用了两天上班摸鱼的时间,总算是把这三个属性基本搞明白了。接下来打算去学习flex: 1、flex: 0等都有什么区别,以及flex的其他特性,如果评论区有小伙伴需要的话我可以将这部分知识也整理出来

最后再次推荐张鑫旭老师的博客,这些年来大佬一直在坚持分享,真的很佩服:www.zhangxinxu.com/wordpress/c…

h5移动端项目总结

2026年1月28日 14:14

移动端项目总结

项目名称:金贝移动端(fe-banner-pub-mobile)
项目定位:面向经纪人的商机购买与管理 H5 平台


📋 项目概述

金贝移动端是一个面向房产经纪人的综合性商机管理平台,提供潜客包、准客包、钻展、商机直通车、CPL/CPT、金贝会员等多种商机产品的购买、管理和跟进服务。项目采用前后端分离架构,前端使用 React 技术栈,后端使用 Node.js 中间层,支持多端(APP 内嵌、H5)访问。

核心业务模块

  • 潜客包/准客包管理:商机包的购买、查看、跟进、转委托、退名额等全流程管理
  • 钻展(抢购、报买):广告位购买与管理
  • 商机直通车:CPA 商机购买
  • CPL/CPT:按线索/按时间计费的商机包
  • 金贝会员:会员权益管理
  • 订单管理:订单列表、详情、支付
  • 数据看板:商机数据统计与分析
  • 预算池:预算管理与分配

🛠️ 技术栈

前端技术栈

技术 版本 用途
React ^16.8.6 UI 框架
Redux ^4.0.1 状态管理
React Router ^5.1.0 路由管理
antd-mobile ^2.3.1 / ^5.34.0 UI 组件库(v2 + v5 混用)
dayjs ^1.8.8 日期时间处理
axios ^0.19.2 HTTP 请求库
webpack ^5.75.0 构建工具
less ^4.1.3 CSS 预处理器
react-hot-loader ^4.3.12 热更新
@antv/f2 ^3.8.6 移动端图表库

后端技术栈

  • Node.js (v12)
  • TypeScript
  • Express/Koa (中间层)
  • Redis (缓存)

开发工具

  • Babel:ES6+ 转译
  • ESLint:代码规范
  • PostCSS:CSS 后处理(px2rem、px-to-viewport)
  • vconsole:移动端调试工具

第三方服务集成


✨ 项目亮点

1. 完整的商机处理闭环设计

业务价值:实现了从商机领取到关闭的全流程管理,提升经纪人工作效率。

技术实现

  • 统一的交互入口:通过 bottomAreaClick 方法统一处理所有商机操作
  • 多场景适配:根据商机来源(opptySource)、类型(新房/二手/租赁)、状态(opptyStatus)动态展示操作按钮
  • 操作类型包括:
    • 加私/转委托:根据 conversionType 动态展示,支持预校验和跳转
    • 联系:区分电话(contactType === 1)和 IM(contactType === 2
    • 记录跟进:弹窗录入,支持局部数据更新
    • 申请退名额:复杂的时间规则和风控校验
    • 查看工单:跳转司南工单系统

代码示例

bottomAreaClick = (e, item, desc, callBack) => {
  const { opptyId, opptyProcessVo, custId } = item
  const { contactType, conversionType } = opptyProcessVo
  e.stopPropagation()
  switch (desc) {
    case '加私':
      this.openXinfang(conversionPageScheme, opptyId)
      break
    case '联系':
      contactType == 2 ? this.imCustomer(custId, opptyId) : this.callTel(opptyId)
      break
    // ... 其他操作
  }
}

2. 复杂状态标签体系的可视化呈现

业务价值:通过标签系统清晰展示商机状态,降低业务理解成本。

技术实现

  • 动态标签生成:根据 conventionLevelisGivenisWeihupan 等状态动态拼装标签数组
  • 交互式标签:点击特定标签(赠送、已联系、未联系、已委托)显示业务提示
  • 样式差异化:不同标签使用不同颜色和样式(如 赠送 红色、维护盘商机 金色背景)

代码示例

// 标签动态生成
if (isGiven && Array.isArray(tags)) {
  tags.unshift('赠送')
}
if (+conventionLevel === -1) {
  tags.push('待委托')
}
if (+conventionLevel === 0) {
  tags.push('已联系')
}
if (+conventionLevel === 1) {
  tags.push('已委托')
}

// 标签点击交互
isGivenClick = (evt, item, shangjiItem) => {
  if (item === '已联系') {
    if (shangjiItem.opptySource === '400') {
      Toast.info('您已拨打过客户电话', 2)
    } else {
      Toast.info('您已回复过客户消息', 2)
    }
  }
}

3. 多端跳转与埋点闭环

业务价值:打通 APP、H5、工单系统等多个平台,实现数据追踪和运营分析。

技术实现

  • 统一埋点封装sendDig(clickId, opptyId) 方法统一上报点击事件
  • 多端跳转封装:通过 Utils 工具类封装拨打电话、IM 联系、跳转客户端等功能
  • 环境区分:根据 window.location.host 区分测试和正式环境,使用不同的跳转链接
  • 埋点数据:包含 opptyIdagent_ucidclick_idc_uicode 等关键字段

代码示例

sendDig(clickId, opptyId) {
  if (window.$ULOG) {
    window.$ULOG.send(this.evtId, {
      event: 'mModuleClick',
      action: {
        opptyId,
        c_uicode: 'qiankebao_qiankebaoliebiao',
        click_id: clickId,
        agent_ucid: window._GLOBAL_DATA.userInfo.id,
      },
    })
  }
}

4. 列表性能与体验优化

业务价值:提升移动端加载速度和用户体验,降低服务器压力。

技术实现

  • 无限滚动:使用 antd-mobile-v5InfiniteScroll 组件实现分页加载
  • 滚动位置保持:记录跟进后恢复滚动位置,避免页面跳回顶部
  • 局部数据更新:通过回调函数 addTempRecordData 更新列表中的单条数据,避免全量刷新
  • 按需插入数据:根据权限和数据返回情况动态插入数据概览卡片

代码示例

// 无限滚动
<InfiniteScroll
  loadMore={() => {
    this.getPackages()
  }}
  hasMore={this.state.packages.pageNum < this.state.packages.totalPage}
  threshold={120}
/>

// 滚动位置保持
record = (data) => {
  this.setState({ textareaModal: false })
  KeFetch(api.saveRecord, { method: 'post', data }).then(() => {
    document.documentElement.scrollTo(0, this.ScroolTop)
    this.genjiCallBack && this.genjiCallBack(data)
  })
}

5. 统一请求封装与错误处理

业务价值:统一 API 调用规范,提升代码可维护性和错误处理能力。

技术实现

  • KeFetch 封装:基于 Ketch 库封装统一请求方法
  • 多环境适配:支持 Node 层(SUCCESS_CODE_NODE: 100000)和 H5 层(SUCCESS_CODE_H5: 1)不同的成功码
  • 统一错误提示:自动处理错误码并展示 Toast 提示
  • 超时控制:默认 20s 超时

🎯 项目难点

难点一:多状态、多来源商机的分支逻辑复杂

问题描述

  • 不同商机来源(OPPTY_POOLSSCCUSTOMER_CLUE400 等)有不同的处理逻辑
  • 不同商机类型(新房/二手/租赁)需要不同的跳转路径
  • 不同场景(OPPTYZHUN_KE_BAOB_PLUSCPS)影响功能展示

解决方案

  1. 统一入口函数:通过 bottomAreaClick 方法统一处理所有操作,内部根据 desc 参数分支处理
  2. 预校验封装:将复杂的校验逻辑抽离成独立方法(如 delegatePrecheckrefundPrecheck
  3. 状态机模式:使用 switch-case 清晰表达不同操作的处理流程
  4. 配置化:通过 getOpptyType() 方法统一获取场景类型,避免重复判断

代码示例

getOpptyType = () => {
  const { type } = Utils.getUrlParams()
  if (type === ZHUN_KE_BAO) return ZHUN_KE_BAO
  else if (type === B_PLUS) return B_PLUS
  else if (type === CPS) return CPS
  else return OPPTY
}

// 转委托预校验
inputCustomer = async (d, conversionPageScheme) => {
  if (d.opptySource == 'OPPTY_POOL' || d.opptySource == 'SSC') {
    KeFetch(api.delegatePrecheck, { data: { opptyId: d.opptyId } })
      .then((data) => {
        if (data.resultCode == 1) {
          // 继续转委托流程
        } else {
          Modal.alert('提示', data.tip || '')
        }
      })
  }
}

收获:通过统一入口和预校验封装,将复杂的业务逻辑集中管理,提升了代码的可维护性和可扩展性。


难点二:时间规则与风控、退款规则耦合

问题描述

  • 申请退名额需要满足多个条件:
    • 商机下发时间需满 25 小时
    • 风控拦截:14 天内提交不通过工单超过 3 次
    • 后端预校验:refundPrecheck 接口返回是否允许退款
  • 时间计算和展示需要精确到秒

解决方案

  1. 时间库统一:使用 dayjs 统一处理所有时间计算和格式化
  2. 状态分离:将不同场景拆解成独立的 Modal 状态(antiCheatModalrefundModal
  3. 链式调用:使用 dayjs(opptyTime).add(25, 'hour') 等链式 API 提升可读性
  4. 用户提示:在弹窗中明确展示截止时间,提升用户体验

代码示例

case '申请退名额':
  const { opptyStatus, opptyTime } = item
  const deadline = dayjs(opptyTime).add(25, 'hour')
  
  if (opptyStatus === 4) {
    // 风控拦截
    this.setState({ antiCheatModal: true })
  } else if (!dayjs().isAfter(deadline)) {
    // 时间未到
    this.setState({
      refundModal: true,
      deadline: deadline.format('YYYY-MM-DD HH:mm:ss'),
    })
  } else {
    // 后端预校验
    KeFetch(api.refundPrecheck, { data: { opptyId } }).then((res) => {
      const { refund, remark } = res
      if (remark) {
        Modal.alert('提示', remark, [/* ... */])
      } else if (!refund) {
        this.onNext(item) // 跳转司南
      }
    })
  }
  break

收获:通过时间库统一和状态分离,将复杂的业务规则清晰地表达出来,便于后续根据运营策略调整。


难点三:列表内局部数据更新与用户滚动位置保持

问题描述

  • 用户在列表中点击「记录跟进」后,需要更新对应商机的 opptyBrief 数据
  • 更新后页面不能跳回顶部,需要保持用户当前的滚动位置
  • 需要支持新增和更新两种场景

解决方案

  1. 滚动位置缓存:在打开弹窗前记录当前滚动位置 this.ScroolTop
  2. 回调函数传递:通过 bottomAreaClickcallBack 参数传递更新函数
  3. 局部状态更新:在子组件中通过 useState 维护列表状态,通过 addTempRecordData 方法更新单条数据
  4. 恢复滚动位置:接口成功后调用 document.documentElement.scrollTo(0, this.ScroolTop)

代码示例

// 父组件:记录滚动位置
case '记录跟进':
  this.genjiCallBack = callBack
  this.ScroolTop = document.body.scrollTop || document.documentElement.scrollTop
  this.genji(opptyId, opptyRemarkVo || {})
  break

// 父组件:恢复滚动位置
record = (data) => {
  this.setState({ textareaModal: false })
  KeFetch(api.saveRecord, { method: 'post', data }).then(() => {
    document.documentElement.scrollTo(0, this.ScroolTop)
    this.genjiCallBack && this.genjiCallBack(data)
  })
}

// 子组件:局部数据更新
const addTempRecordData = recordData => {
  const newData = opptyListState.map(item => {
    if (item.opptyId === recordData.opptyId) {
      if (Array.isArray(item.opptyBrief)) {
        const result = item.opptyBrief.filter(item2 => item2.name === '跟进反馈')
        if (result.length <= 0) {
          item.opptyBrief.push({
            name: '跟进反馈',
            desc: `${recordData.opptyFlagName},${recordData.remark}`,
          })
        } else {
          item.opptyBrief.map(item2 => {
            if (item2.name === '跟进反馈') {
              item2.desc = `${recordData.opptyFlagName},${recordData.remark}`
            }
            return item2
          })
        }
      }
    }
    return item
  })
  setOpptyListState(newData)
}

收获:通过滚动位置缓存和回调函数机制,实现了局部数据更新和用户体验的平衡,避免了全量刷新带来的性能问题。


难点四:老项目技术栈混用带来的兼容问题

问题描述

  • 项目中同时使用 antd-mobile v2 和 v5 两个版本
  • 类组件和函数组件混用(QianKe 是类组件,waitDeal 是函数组件)
  • 需要兼容不同版本的 API 和组件特性

解决方案

  1. 渐进式升级:新功能使用新版本(如 InfiniteScroll 使用 v5),老功能保持原样
  2. 统一接口设计:通过 props 和回调函数统一组件间的通信接口
  3. 工具函数封装:将通用逻辑抽离成工具函数,避免重复代码
  4. 文档记录:在代码注释中标注版本差异和注意事项

代码示例

// 使用 v5 的 InfiniteScroll
import { InfiniteScroll } from 'antd-mobile-v5'

// 使用 v2 的 Modal、Toast
import { Modal, Toast } from 'antd-mobile'

// 统一回调接口
<QkbOrder
  data={item}
  onClick={this.itemClick}
  onSpeedClick={this.executeSpeed}
  bottomAreaClick={this.bottomAreaClick}
  purposeValiageClick={this.purposeValiageClick}
/>

收获:通过渐进式升级和统一接口设计,在保证项目稳定性的同时,逐步引入新技术,为后续整体重构打下基础。


📁 项目结构

fe-banner-pub-mobile/
├── client/                    # 前端代码
│   ├── src/
│   │   ├── components/        # 公共组件
│   │   │   └── QkbOrder/      # 潜客包订单组件
│   │   │       └── component/
│   │   │           └── waitDeal.js  # 待处理商机列表
│   │   ├── containers/        # 页面容器
│   │   │   └── ShangJi/       # 商机模块
│   │   │       ├── Package/   # 我的商品
│   │   │       │   └── Qianke/  # 潜客包页面
│   │   │       ├── Home/      # 首页
│   │   │       ├── Data/      # 数据看板
│   │   │       └── Order/     # 订单管理
│   │   ├── config/            # 配置文件
│   │   │   ├── apiConfig.js   # API 配置
│   │   │   └── digConfig.js   # 埋点配置
│   │   ├── utils/             # 工具函数
│   │   │   ├── keFetch.js     # 请求封装
│   │   │   └── storage.js     # 本地存储
│   │   ├── router/            # 路由配置
│   │   ├── store/             # Redux store
│   │   └── App.js             # 根组件
│   ├── webpack/               # Webpack 配置
│   └── package.json
├── server/                     # Node.js 中间层
│   ├── src/
│   │   ├── apis/              # API 接口
│   │   ├── actions/           # 业务逻辑
│   │   └── configs/           # 配置文件
│   └── package.json
└── README.md

🔧 核心功能实现

1. 潜客包列表加载

getPackages(Kdata) {
  const pageNum = (this.state.packages.pageNum || 0) + 1
  const scene = this.getOpptyType()
  
  KeFetch(api.getPackages, {
    data: {
      cityCode: workCity(),
      pageSize: 10,
      pageNum,
      scene,
    },
  }).then((data) => {
    // 根据权限动态插入数据概览
    if (data.statisticsShow && Kdata && scene === OPPTY) {
      const hasDataOverviewPermission = (window._GLOBAL_DATA.userInfo.perms || [])
        .includes('BRAND_M_qianke_dataOverview')
      if (!this.hasInsert && hasDataOverviewPermission) {
        data.list.unshift({ ...Kdata, type: 'qkDataOverview' })
        this.hasInsert = true
      }
    }
    
    data.list = [...(this.state.packages.list || []), ...data.list]
    this.setState({
      packages: data,
      loading: false,
    })
  })
}

2. 虚拟号码拨打

callTel = (opptyId) => {
  this.sendDig(10016, opptyId)
  KeFetch(api.getShangjiVirtualPhone, { data: { opptyId } })
    .then((data) => {
      if (data.virtualPhone) {
        Modal.alert(
          `客户电话${data.virtualPhone}`,
          '此号码为虚拟号码,非客户真实号码',
          [
            { text: '取消', onPress: () => {} },
            { text: '立即拨打', onPress: () => Utils.callTelphone(data.virtualPhone) },
          ]
        )
      }
    })
}

3. 转委托流程

inputCustomer = async (d, conversionPageScheme) => {
  // 预校验
  if (d.opptySource == 'OPPTY_POOL' || d.opptySource == 'SSC') {
    const precheckData = await KeFetch(api.delegatePrecheck, {
      data: { opptyId: d.opptyId },
    })
    if (precheckData.resultCode !== 1) {
      Modal.alert('提示', precheckData.tip || '')
      return
    }
  }
  
  // 执行转委托
  const res = await KeFetch(api.delegateQiankeOpportunity, {
    data: { opptyId: d.opptyId },
  })
  
  if (res.result) {
    Modal.alert('提示', res.resultDesc || '', [
      {
        text: '去编辑委托',
        onPress: () => {
          Utils.inputCustomer(() => {
            Modal.alert('请先将客户端升级至最新版本', '', [
              { text: '取消' },
              { text: '立即升级', onPress: () => Utils.openAbout() },
            ])
          }, res.conversionPageScheme)
        },
      },
    ])
  }
}

📊 性能优化

  1. 代码分割:使用 Webpack 的 code splitting 按路由分割代码
  2. 图片优化:使用 url-loaderfile-loader 处理图片资源
  3. CSS 优化:使用 mini-css-extract-plugin 提取 CSS,使用 px2rem 适配移动端
  4. 无限滚动:使用 InfiniteScroll 实现分页加载,避免一次性加载大量数据
  5. 局部更新:通过回调函数更新局部数据,避免全量刷新

🐛 常见问题与解决方案

1. 鸿蒙系统 Picker 组件滑动穿透

问题:antd-mobile v2 的 Picker 组件在鸿蒙系统滑动选择时会有滑动穿透 bug。

解决方案:使用 components/WithTouchMoveControlHoc 高阶组件包裹 Picker 组件。

2. Node 版本兼容

问题:client 端使用 Node 14,server 端使用 Node 12。

解决方案:使用 nvm 进行 Node 版本切换,或在启动脚本中自动切换。

3. VPN 冲突

问题:Node 层加了 Redis 之后,挂了外网 VPN 项目启动不起来。

解决方案:关闭 VPN 后再启动项目。


🚀 部署流程

  1. 开发环境
    npm install
    npm start  # 自动切换 Node 版本并启动前后端
    

📝 总结

技术收获

  1. 复杂业务逻辑处理:通过统一入口函数和预校验封装,将复杂的业务规则清晰地表达出来
  2. 用户体验优化:通过滚动位置保持、局部数据更新等技术手段,提升用户操作体验
  3. 多端适配:通过 JSBridge 和工具函数封装,实现 APP 和 H5 的统一适配
  4. 性能优化:通过无限滚动、代码分割等技术,提升页面加载速度和运行性能

业务理解

  1. 商机管理全流程:深入理解了从商机产生到关闭的完整业务流程
  2. 风控规则:理解了时间规则、风控拦截等业务规则的设计思路
  3. 数据埋点:理解了埋点数据对运营分析和产品优化的重要性

项目价值

  1. 提升效率:通过完整的商机处理闭环,大幅提升经纪人的工作效率
  2. 规范操作:通过统一的交互入口和校验逻辑,规范了业务操作流程
  3. 数据支撑:通过完善的埋点体系,为后续的运营分析和策略优化提供了数据支撑

📚 相关文档


最后更新:2024年

曦望发布新一代推理芯片S3

2026年1月28日 14:13
36氪获悉,国产GPU厂商曦望(Sunrise)发布新一代推理芯片S3。在算力与存储设计上,S3支持从FP16到FP4的精度自由切换,并率先在国内GPGPU产品中采用LPDDR6 显存方案,显存容量较上一代提升4倍,缓解了大模型推理中常见的显存瓶颈问题。在DeepSeek等主流大模型上,其单位Token推理成本较上一代降低约90%。

徕卡 10 亿欧元卖身,为什么小米一定不能买?

作者 周奕旨
2026年1月28日 14:10

相机界的「劳力士」,又一次摆上了货架。

这周,影像圈最大的新闻莫过于此:徕卡又要卖了。

彭博社传来消息,徕卡控股方正考虑出售股权,估值约 10 亿欧元。消息一出,圈内错愕:M 系列一机难求,手机联名风生水起,怎么偏在「巅峰期」卖身?

更有意思的是国内的呼声:「雷总,别犹豫,买下它!」

这种情绪不难理解——毕竟这两年小米和徕卡的联名搞得风生水起,既然关系这么好,干脆领证结婚岂不美哉?

且慢,这笔账,恐怕不能这么算。

豪门里的生意经

目前的徕卡,股权结构其实挺简单:奥地利的考夫曼家族(ACM 项目开发公司)持有 55% 的股份,是话事人;美国的私募巨头黑石集团持有 45% 的股份。

这次传出要卖的,就主要是后者手里的那 45%。

为什么要卖?是徕卡不行了吗?

恰恰相反,是因为徕卡现在「太行了」。

在奢侈品和投资圈,有个不成文的规矩:最好的套现时机,不在低谷,而在巅峰。在这个世界上,私募基金(PE)的逻辑从来都不是做慈善,或者是守护什么人类光学遗产,他们的逻辑简单粗暴:低买,高卖。通常 PE 的投资周期是 5 到 7 年,但 2011 年,黑石以 1.3 亿欧元拿下徕卡 45% 的股份,一拿就是 13 年,已经远远超出了常规范畴,直到最近,徕卡刚交出了一份史上最漂亮的财报——去年营收接近 6 亿欧元。

黑石认为,完美的下车时机来了。

手里的一支股票,拿了十几年,翻了好几倍,现在正好涨到历史最高点,是继续拿着情怀,还是落袋为安?

显然,黑石选择了后者。

消息中还提到,除了黑石集团,作为大股东的考夫曼家族也可能顺势调整手中的筹码——这是现代徕卡的灵魂角色。

要读懂今天的徕卡,绕不开安德烈亚斯·考夫曼,没有他,徕卡的故事在二十年前恐怕就剧终了。

▲ 安德烈亚斯·考夫曼

2004 年前后,数码浪潮席卷全球。当佳能和尼康拿着数码单反大杀四方时,徕卡守着胶片时代的余晖。银行断贷、高管换血、抵押工厂,这位德国光学巨人步履蹒跚,一只脚已经踏进了坟墓,不得已,推出了大量价格相对低廉、由日企代工的胶片傻瓜机,这些产品虽然走量,但严重稀释了徕卡的品牌格调,让用户觉得花钱买了个标。

就在这生死关头,考夫曼——这位左派教师出身的「富二代」,带着家族资金入场,并做了一个当时被华尔街视为「疯了」的决定:All in 徕卡,而且要彻底数字化。

▲ 幸存的和松下合作的 D-Lux 也被定义为高端便携机

考夫曼最毒辣的眼光,在于他为徕卡重新确立了坐标:与其做大众的相机,不如做奢侈品。

他砍掉讨好市场的廉价线,专注于 M 系列的数字化,并在此基础上,重塑认知——买徕卡不再仅仅为了拍照,而是为了展示「我有品位」。

▲ Leica 的数码 M 系列开端 M8

在这场长达 20 年的品牌重塑中,考夫曼步步为营,先在 2011 年引入黑石集团,获得华尔街资本的背书,又在次年推出只能拍摄黑白照片的 M Monochrome,巩固了其对摄影文化的解释权,仅仅两年后,又将公司迁回徕卡的诞生地,打造 Leitz Park,建立起信徒的朝圣总部。

引援、立威、筑庙,一套精神图腾的必要元素都备齐了,徕卡正式起飞。

市场证明了考夫曼的战略眼光,由卖产品的思路转为卖品牌后,如今的徕卡,活成了相机界的劳力士。

眼下,黑石准备抛售 45% 的股份,若考夫曼家族也顺势调整,那么这笔交易极可能产生过半的股权变动——对于一家未上市的私企而言,过半的股份变动涉及绝对控股权的易主,重要性不言而喻。

但正是这种「奢侈品化」的成功,也隐隐注定了它的下一任主人,绝不能是那些急于求成的科技新贵。

百年红点,花落谁家?

黑石退场,谁来接手?

彭博社提到了几个名字,包括之前的红杉中国(现 HSG),还有瑞典的私募股权公司 Altor Equity Partners,后者是 Marshall Group 的第二大股东,很擅长经典老牌现代商业化的复杂转型。

▲ 没错,就是这个音响

但在中国互联网上,被提及最多的却是另一个名字:小米。

「雷总,这不得冲一波?」 「买下来后,小米就是徕卡正统了」

大家有这种想法很正常,小米 12S Ultra 之后的几代产品,因为徕卡的加持,影像实力突飞猛进,高端路走得越来越稳,最新的小米 17 Ultra 徕卡版,也将两者的合作提升到了新高度。手里握着千亿现金储备的小米,买下 10 亿欧元的徕卡,从钱的角度看,也确实是「洒洒水」。

但是,我得泼一盆冷水:收购徕卡,对小米来说,可能是一笔双输的买卖。

其中的逻辑关键,在于品牌势能——小米与徕卡的联姻之所以成功,核心在于「借势」。

这条钻研品牌的路,始于华为与徕卡的合作,P9 一出世 ,德味天下知。

而作为科技新贵,小米拥有极高的效率和技术积累,但在影像审美和品牌厚度上,难免显得「年轻」。而徕卡作为百年贵族,手里握着的是关于「摄影文化」的解释权和话语权。

小米给徕卡交昂贵的「学费」,换取的是徕卡对影像系统的调教,以及红色 Logo 带来的高端认证。这是一种平等的、甚至略带仰视的合作,也就是在前不久,品牌上的合作又来到新的高度,徕卡高度介入小米 17 Ultra By Leica 版本的打造中,全力主导了机身设计、徕卡一瞬的开发工作。

可以说,这是双方合作以来,徕卡「含金量」最高、灵魂渗透最深的一次。

▲ 由小米 17 Ultra by Leica 的徕卡一瞬拍摄

可以想像,一旦收购,这种张力就会崩塌。如果劳力士变成苹果的手表部门,还能维持溢价吗?

互联网公司信奉的是「快」,是敏捷迭代、极速试错;而徕卡赖以生存的根基是「慢」,是近乎偏执的传承,两种价值观相去甚远,甚至背道而驰。当神变成了员工,神格破碎,徕卡的品牌说服力直线下降,小米也随之失去一个可靠品牌的背书。

况且,徕卡目前的商业模式非常灵活,它既教小米和影石做影像,也和松下搞 L 卡口联盟,还为 iPhone 设计了一个套徕卡滤镜的 Leica Lux,单一年的订阅费用,都需要 500 元。

若是直接归属某一家手机厂商,这种开放性势必会被排他性取代,路反而越走越窄。

也许有人会搬出「大疆收购哈苏」的成功案例来反驳。

确实,大疆收购了哈苏的大部分股权。但这有个前提:当时的哈苏是真的快不行了,而且大疆急需哈苏来提升无人机的品牌壁垒,可以说是珠联璧合,而且,大疆在收购后非常克制,小心翼翼地维护着哈苏的高端形象,只在御 Mavic 旗舰型号上打上 Hasselblad 的标。

即便如此,你也必须承认,现在提到哈苏,大家的第一反应已经不完全是当年那个登月的传奇,而是大疆相机。

▲ 大疆 Mavic 4 Pro 上的哈苏 logo

对于徕卡这种靠「腔调」和「文化」溢价生存的品牌,任何带有强工业属性、强消费电子属性的母公司,不仅无法通过收购获得品牌溢价,反而可能稀释掉它最珍贵的那部分资产。

所以,徕卡最好的归宿在哪里?

大概率,还是像 Altor 这样的私募股权基金,或者是某个像考夫曼一样,既有钱又有闲,还对摄影有某种执念的家族基金。他们是沉默的赞助人,提供资金、要求回报,但不干涉你在这个镜头里是用萤石还是玻璃,不强迫你在机身上印上「Powered by…」。

它应该属于资本,因为它需要钱来生存;但它不该完全属于某一家科技巨头,因为它需要保持距离,才能维持神话。

至于那个「小米收购徕卡」的愿望,就让它留在互联网的段子里吧,有些神坛上的东西,还是让它留在神坛上比较好。

让我有个美满旅程

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


Anthropic上调未来数年营收预测

2026年1月28日 14:09
1月28日,据报道,美国人工智能初创企业Anthropic已大幅上调了未来数年的营收预测,预计今年销售额将增长四倍,达180亿美元,2027年将达550亿美元。另有报道称,Anthropic以3500亿美元的估值完成其最新一轮融资,融资金额远超最初的100亿美元目标。(界面)

老项目 Vue2 函数式打开弹层【附源码】

2026年1月28日 13:59

大家好,我是前端架构师,关注微信公众号【@程序员大卫】免费领取前端精品资料。

前言

在老项目(Vue2 + ElementUI)里写弹层,大家一定很熟悉下面这种写法:

<dialog-test
  v-if="dialogVisible"
  :text="text"
  @confirm="handleConfirm"
  @close="dialogVisible = false"
/>

问题是:

  • 每个页面都要写一堆 visible
  • 关闭弹层还得手动销毁组件
  • 多个弹层时,代码又臭又长
  • 只是想 “点个按钮弹个框”,却要改一堆模板

所以我在老项目里封装了一个 函数式打开弹层 的方法:

  • 不用写 template,不用定义变量 dialogVisible
  • 直接 JS 调用
  • 自动创建 / 销毁
  • 支持缓存,不销毁反复用

最终效果

像这样直接调用即可:

dialogOpen(
  DialogTest,
  {
    text: "Hello World!",
    onConfirm: (text) => {
      console.log(text);
    },
  },
  { context: this },
);

一句话总结:

把“写组件”这件事,变成“调函数”

一、弹层组件本身(DialogTest.vue)

这个组件本身非常普通,没有任何黑魔法。

<template>
  <el-dialog title="提示" visible width="30%" @close="doClose" append-to-body>
    <span>{{ text }}</span>

    <span slot="footer" class="dialog-footer">
      <el-button type="primary" @click="doCancle">取消</el-button>
      <el-button type="primary" @click="doConfirm">确 定</el-button>
    </span>
  </el-dialog>
</template>

关键点说明

  • visible 由外部控制(函数式传入)
  • 所有行为通过 $emit 往外抛
export default {
  name: "DialogTest", // ⚠️ 必须有 name,后面做缓存用
  props: {
    text: String,
  },
  methods: {
    doClose() {
      this.$emit("close");
    },
    doCancle() {
      this.doClose();
    },
    doConfirm() {
      this.$emit("confirm", "点击确认按钮");
      this.doClose();
    },
  },
};

记住一句话:组件只负责展示和抛事件,不管怎么被创建。

二、核心思路:函数式弹层是怎么实现的?

一句话概括实现原理:

new Vue() 在 JS 里动态创建组件,并手动挂载到 body 上

也就是说,我们不再依赖 template,而是:

  1. JS 创建 Vue 实例
  2. render 一个 Dialog 组件
  3. 挂载到 DOM
  4. 关闭时销毁或隐藏

三、dialogOpen.js 整体结构说明

我们先看函数签名:

dialogOpen(Component, props = {}, options = {})

三个参数分别是:

参数 说明
Component 弹层组件
props 传给组件的 props + 事件
options.context 当前页面的 this
options.destroyOnClose 关闭是否销毁(默认 true)
options.key 缓存 key

四、为什么必须传 context?

dialogOpen(DialogTest, {...}, { context: this })

这是很多人第一次看不懂的地方。

原因只有一个:

让弹层组件继承当前页面的 $router$store 等上下文

new Vue({
  parent: context
})

否则:

  • $router 可能是 undefined
  • $store 用不了
  • inject / provide 失效

五、props 和事件是如何区分的?

调用时我们这样写:

dialogOpen(DialogTest, {
  text: "Hello World",
  onConfirm: () => {},
  onClose: () => {}
})

那问题来了:

  • 哪些是 props?
  • 哪些是事件?

解决方案:统一约定

onXxx => 事件
其它 => props

对应代码:

const splitPropsAndListeners = (input = {}) => {
  const props = {};
  const on = {};

  Object.keys(input).forEach(key => {
    if (/^on[A-Z]/.test(key) && typeof input[key] === 'function') {
      const event = key.slice(2).replace(/^[A-Z]/, s => s.toLowerCase());
      on[event] = input[key];
    } else {
      props[key] = input[key];
    }
  });

  return { props, on };
};

最终会变成:

h(Component, {
  props: { text },
  on: { confirm, close }
})

六、真正创建弹层的地方(核心代码)

let wrapperVm = new Vue({
  parent: context,
  data() {
    return {
      rawProps: initialProps
    };
  },
  computed: {
    vnodeData() {
      const { props, on } = splitPropsAndListeners(this.rawProps);
      return { props, on };
    }
  },
  render(h) {
    return h(Component, {
      props: this.vnodeData.props,
      on: {
        ...this.vnodeData.on,
        close: (...args) => {
          this.vnodeData.on.close?.(...args);
          closeHandler();
        }
      }
    });
  }
});

这里发生了什么?

  1. rawProps 保存所有传入参数
  2. computed 动态拆分 props / 事件
  3. render 手动渲染组件
  4. 拦截 close,统一处理销毁逻辑

七、弹层是怎么挂载到页面上的?

let container = document.createElement('div');
document.body.appendChild(container);
wrapperVm.$mount(container);

👉 这一步相当于:

<body>
  <div>
    <!-- Dialog 组件 -->
  </div>
</body>

八、关闭时销毁 vs 不销毁(缓存机制)

默认行为(destroyOnClose = true)

wrapperVm.$destroy();
removeElement(wrapperVm.$el);

好处:

  • 不占内存
  • 最安全

开启缓存(destroyOnClose = false)

const instanceCache = new Map();
  • 同一个 key 只创建一次
  • 关闭时只是 visible = false
  • 再次打开直接复用
if (!destroyOnClose && instanceCache.has(key)) {
  const cacheWrapperVm = instanceCache.get(key);
  cacheWrapperVm.updateProps(initialProps);
  return cacheWrapperVm.triggerClose;
}

适合:

  • 表单弹层
  • 频繁打开的弹窗

九、为什么要监听页面销毁?

context.$once('hook:beforeDestroy', destroy);

防止:

  • 页面销毁了
  • 弹层还留在 body
  • 造成内存泄漏

十、总结

这个方案适合:

  • Vue2 老项目
  • ElementUI 弹层
  • 不想每个页面都写 dialog

附源码

github.com/zm8/wechat-…

不想上班,所以我写了个能搞钱的工具

2026年1月28日 13:53

大家好,我是凌览。

如果本文能给你提供启发或帮助,欢迎动动小手指,一键三连(点赞评论转发),给我一些支持和鼓励谢谢。


还记得钉钉提示音让心脏漏跳半拍的感觉?还记得周五火锅吃到一半,弹出那句"不急,周一给我"的绝望?

每个月总有那么几个瞬间,手指悬在"离职申请"上,想把那些"抓手赋能""链路闭环"的黑话统统砸回老板脸上,拉着姑娘直奔海边,让海风把KPI、OKR统统吹散。

但深夜算完银行卡、账单和退休政策后,现实很骨感:按现在的攒钱速度,我得打工打到退休。

盯着满桌演算草稿,我突然开窍:我是个开发者啊! 既然距离自由还有十万八千里,为什么不开发个产品赚睡后收入,让代码替我加速跑路?

之前做自媒体剪视频,最烦的就是找素材——好不容易从平台扒下来的视频,中间总挂着碍眼的水印,关键帧根本没法用。

与其到处求无水印资源,或是开着 PS 一帧帧抠图,我干脆一拍键盘:不如自己写个工具,一键把水印抹干净。

调研

有了想法肯定得调研可不可行,找找竞品有哪些问题。

总结下来,竞品蛮多,当然问题也多。

调研完发现竞品问题很集中:

  1. 个人开发居多,打开就是输入框+按钮,用户不知道怎么用
  2. 企业级产品强制开会员,成本就上来了
  3. 大部分为小程序,对于PC端并不支持
  4. 扎堆传统平台,AI视频/图片的新平台完全空白

这四点全是机会,不同质化竞争,做差异化。

设计+开发

首先,我不是设计师,UI 这块没什么专业见解。具体做法是先扒一遍竞品,在巨人肩膀上修修补补。

技术栈没折腾,直接上熟练工:Vue3 + Tailwind + UniApp + Node.js。本职前端,后端找了个 24 小时在线的「赛博同事」——AI 负责写,我负责 Review 和改 Bug(毕竟底子还在,只是生锈了)。

坚持能简则简,什么高并发、什么数据库等选择能省则省。比如其中有个 IP 限流的功能,直接本地 JSON 文件临时存储,足够用了。

主页面 UI 如下:

11.png

上线与收益

因 wx 平台审核导致小程序名称与 PC 端品牌名被迫不一致,更繁琐的是整个上线前的各种审核——备案、公安备案、企业认证,层层审核确实消耗了大量耐心,这步不详细描述跳过。

上线后,我把它分享给了我的朋友,也得到了很多人的认可。

gh_cb56a387b645_258.jpg

聊聊收益,其实我有三条来钱路子。今天先聊最「躺」的那个——流量主。

流量主有个「新手村」门槛:500 个累计用户。跨过去之后倒是省心,平台把广告组件都打包好了,接起来贼方便。 PC 端本来也想挂 Google Ads 赚点美刀,但一看要填表、申请、过审……算了,拖延症犯了,至今没搞。主要是嫌烦,先放着吧。

image.png

收入不多,但每天一根火腿肠是没问题的。推广到位单靠流量主一个月也能有300,每年只需要支付认证费用30再加服务器成本,对于我来说还是赚的。

最后

当然,这个工具还不足以让我实现不想上班的愿望,还在努力中。

至少现在我每天能多吃一根火腿肠了!!!

国家移民管理局:2025年多项出入境创新举措实施

2026年1月28日 13:47
国家移民管理局今天召开新闻发布会介绍,2025年全国共6.97亿人次出入境,同比上升14.2%,创历史新高。记者从发布会上了解到,2025年,我国实施一系列支持扩大开放服务高质量发展创新举措,为高水平开放、高质量发展注入新活力新动能。2025年在促进中外人员交流交往方面,扩大内地居民换发补发出入境证件“全程网办”试点城市范围,总数增至50个。(央视新闻)

前端向架构突围系列 - 浏览器网络 [5 - 5]:Web Vitals 性能指标体系与全链路

2026年1月28日 13:42

写在前面

很久以前,我们用 window.onload 和“白屏时间”来衡量性能。但在单页应用(SPA)和骨架屏盛行的今天,这些老指标已经失效了。页面“加载完”了(Spinning loader 消失),但内容可能还没出来;内容出来了,可能点不动;点得动了,广告突然弹出来把你正在看的文章挤跑了。

2020 年,Google 推出了 Web Vitals,重新定义了用户体验的度量衡。

这一节,我们将手中的“秒表”换成精密的“心电图机”,不仅要让页面跑得快(LCP),还要跑得稳(CLS),更要反应灵敏(INP)。

image.png


一、 核心指标三巨头:LCP、INP 与 CLS

Google 在几十个性能指标中,钦点了三个作为 Core Web Vitals (CWV) 。这不仅关乎用户体验,还直接影响 SEO 排名

1.1 LCP (Largest Contentful Paint) —— 视网膜的愉悦

  • 含义: 视口内最大的那块内容(通常是大图或 H1 标题)渲染完成的时间。
  • 为什么不用 load 事件? 因为 load 触发时,屏幕可能还是白的,或者只有一个 Loading 圈。用户不在乎 Loading 圈,用户在乎的是看到正文。
  • 及格线: 2.5 秒以内。

1.2 INP (Interaction to Next Paint) —— 指尖的快感

  • 注意: 以前叫 FID (First Input Delay),2024 年 3 月起已被 INP 正式取代。架构师必须更新知识库!
  • 含义: 并不是测你第一次点击有多快,而是测全生命周期内,页面对用户操作(点击、按键)的响应延时(从点击到下一帧绘制的时间)。
  • 及格线: 200 毫秒以内。
  • 底层逻辑: 如果 INP 高,说明主线程被长任务(Long Task)堵死了(回顾第四篇 Event Loop)。

1.3 CLS (Cumulative Layout Shift) —— 视觉的稳定

  • 含义: 累积布局偏移。通俗说就是“页面跳不跳”。
  • 场景: 你正要点“取消”,突然顶部加载出来一张广告图,把你挤到了下面的“确认”按钮上。这是最让用户抓狂的体验。
  • 及格线: 0.1 分以下。

二、 实验室数据 vs 真实用户数据:你被 Lighthouse 骗了吗?

很多开发者在本地跑 Lighthouse 拿了 100 分,上线后用户却骂声一片。为什么?

2.1 Lab Data (实验室数据 / 合成监控)

  • 工具: Lighthouse, Chrome DevTools Performance。
  • 环境: 你的高配 MacBook Pro + 公司千兆光纤。
  • 特点: 环境可控,适合调试,但不代表真实体验

2.2 Field Data (现场数据 / 真实用户监控 RUM)

  • 工具: Chrome UX Report (CrUX), 埋点上报。
  • 环境: 用户的红米手机 + 地铁里的弱网 4G。
  • 特点: 这才是真相

架构师策略: “用 Lab Data 治未病,用 Field Data 治已病。” 你需要在 CI/CD 流水线中跑 Lighthouse 守住底线,同时在生产环境接入 RUM (Real User Monitoring) 收集真实用户的 Web Vitals。

// 生产环境监控实战:使用 web-vitals 库
import { onLCP, onINP, onCLS } from 'web-vitals';

function sendToAnalytics(metric) {
  const body = JSON.stringify(metric);
  // 使用 navigator.sendBeacon 保证页面关闭时也能发送
  navigator.sendBeacon('/analytics', body);
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);

三、 全链路优化实战:串联前四篇的知识

现在,我们拿着这三个指标,回顾前四节的内容,看看如何对症下药。

3.1 优化 LCP (加载速度) —— 考验“管道”与“守门”

LCP 慢,通常是因为资源下不来,或者渲染被阻塞。

  • 回顾第一篇 (网络): 升级 HTTP/3 (QUIC) ,消除队头阻塞,加速握手。

  • 回顾第二篇 (资源):

    • CDN 预连接: <link rel="preconnect" ...>
    • 关键资源预加载: 对 LCP 图片使用 <link rel="preload" as="image" ...>
    • Fetch Priority: <img src="hero.jpg" fetchpriority="high">

3.2 优化 INP (交互响应) —— 考验“心脏”

INP 差,说明主线程太忙,Event Loop 转不动了。

  • 回顾第四篇 (Event Loop):

    • 切片: 只有把长任务切碎(Time Slicing),主线程才有空隙去响应用户的点击。
    • Web Workers: 把繁重的计算(如加密、大文件解析)扔出主线程。
    • 避免 Layout Thrashing: 别在点击事件里强制读取 offsetWidth 导致同步重排。

3.3 优化 CLS (视觉稳定) —— 考验“画师”

CLS 高,是因为画师在画布上反复涂改。

  • 回顾第三篇 (渲染):

    • 定尺寸: 所有的 <img><video> 必须写死 widthheight 属性(或 CSS aspect-ratio),先占位,后加载。
    • 字体抖动: 使用 font-display: swapoptional,避免 FOIT (Flash of Invisible Text)。
    • 动画合成: 坚持使用 transform 做动画,避免触发布局变化。

四、 性能文化的建设:从“突击队”到“正规军”

作为架构师,最难的不是自己修 Bug,而是防止别人写 Bug。

4.1 性能预算 (Performance Budget)

在 Webpack/Vite 配置中设置阈值:

  • JS Bundle 体积不得超过 200KB。
  • 关键 CSS 不得超过 50KB。 一旦超标,构建直接失败。

4.2 自动化守门员

在 GitHub Actions 或 Jenkins 中集成 Lighthouse CI。 如果是 PR 导致 Performance 分数下降了 5 分,禁止 Merge。


结语:快,是做出来的,更是守出来的

性能优化不是一种“魔法”,而是一门“工程学”。 它需要你理解从 TCP 包的发送,到 CPU 的调度,再到像素合成的每一个环节。Web Vitals 就是这套复杂系统的仪表盘。

至此,第五阶段《浏览器运行原理 + 前端网络协议与请求模型》 圆满结束。 我们深入了管道,拜访了守门员,观摩了画师,解剖了心脏,最后拿起了度量尺。

现在,你的前端应用已经跑得飞快了。但是,代码写得快、跑得快就够了吗?如果代码乱得像一团麻,或者上线就崩,再快也是徒劳。

宁德时代在内蒙古新设储能科技公司

2026年1月28日 13:42
36氪获悉,爱企查App显示,近日,内蒙古蒙宁时代储能科技有限公司成立,法定代表人为姚天明,经营范围包括储能技术服务、电池零配件销售、人工智能基础软件开发、网络与信息安全软件开发、人工智能应用软件开发等。股权穿透图显示,该公司由宁德时代新能源科技股份有限公司间接全资持股。
❌
❌