阅读视图

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

2026第一站:分享我在高德大赛现场学到的技术、产品与心得

一、引言

1月9日,我参加了高德空间智能开发者大赛决赛,因提交的作品获得了优胜奖,所以受到官方邀请来现场,可以与更多开发者面对面交流,开发者大会既是展示技术的舞台,也是交流碰撞的场所。

下面,我按照现场流程和主题,分享我的参会见闻、优秀项目亮点和一些技术心得,供大家参考。

image.png

二、参会经历

1、行前准备与抵达

我提前一天入住了大赛项目组给安排好的苏州市国际会议酒店,第二天到会场就能感受到主办方在流程和细节上的专业。大家签到有序,现场的问题咨询与临时需求都能及时处理,为开发者紧凑的一天提供了良好保障。

image.png

image.png

2、会场氛围与组织

会场把展示、答辩和交流区划分得很清楚,观众流线顺畅,方便评委和观众近距离互动。每支队伍都有固定演示时间,时间控制严格但合理,有助于突出技术亮点和应用场景。

打卡区:投个飞机

image.png

打卡区:姿势生成

image.png

打卡区:热印工坊

image.png

打卡区:抽个彩蛋

image.png

毫不夸张的说,亲眼目睹了现场好多同学中了华为手环

3、作品演示与答辩(核心看点)

本届比赛涌现了许多把 AI 能力和位置服务(LBS)深度结合的好点子,下面挑几个我认为比较有意思的做个简介:

1、AI+LBS 智能露营寻址助手

这个项目超越了传统的 POI 检索,结合地图与 AI 的图像/语义分析,能发现地图上未标注但适合露营的空地。强调可解释性:不仅给出推荐地点,还能说明理由(比如地形、遮蔽、距离等)。实现上融合了空间特征提取、语义分类和安全性判断。

image.png

image.png

2、“15分钟生活圈”与双层 AI 助手

方案把用户需求分为“生活类”和“商业类”两条链:生活类走轻量推荐以保证速度;商业类触发深度分析,做竞品检索、热度评估并输出选址报告。关键技术有意图识别、检索与生成的动态 RAG 策略,以及对 Token 和成本的优化。

image.png

image.png

3、智慧农业:一图管全局的“高高种地”

把农业管理功能做在一张可视化地图上,包含地块管理、气象、路径导航和农事记录。高精度地块依赖 LBS,图像识别用于作物和病虫害检测,目标是让农业管理更精细、可追溯,方便决策。

image.png

4、“爪爪足迹”智能遛狗小助手

针对不同用户(如精致养宠青年、新手主人、老年人养犬),系统提供个性化遛狗路线、宠物友好 POI、天气与健康提醒,并通过打卡与成就体系提升粘性。项目充分利用了 POI 搜索和路径规划,重视长期行为设计。

image.png

5、盲人导航的社会价值实现

这是技术与公益结合的典型案例:团队把导航从“算路线”扩展为“实时安全决策 + 语音陪伴”。思路是端到端多模态闭环(语音→意图理解→空间 API 调用→决策生成→语音反馈),并接入行走态的实时环境感知(如连续帧的 YOLOv8 检测),把视觉识别结果转成可听的风险提示。

image.png

6、云端旅游“星途旅人”

基于高德地图数据和 AI 虚拟分身,提供“身未动而心已远”的云端旅行体验。难点在于怎么呈现沉浸感和设计可交互的内容。

image.png

当然还有很多其他优秀作品,无法逐一列举,但都展示了 AI 与 LBS 结合的巨大潜力。

三、与行业大咖面对面

下午是颁奖和大咖分享环节,几位专家的演讲很有干货,我记了几点要点:

1、高德“时空智能百宝箱”——产品总监分享

高德的核心是解决“我在哪、要去哪、怎么去”的问题,分享聚焦定位 SDK、POI 检索、地理围栏和 AI Agent,强调把这些能力组件化、工具化,方便开发者在不同场景中快速复用。

image.png

image.png

image.png

image.png

2、AI 时代的地图与定位——定位系统首席专家

演讲讲了导航的演进:从基础地图到车道级定位,再到实时事件感知(比如急刹车检测)。地图正从静态数据向实时感知与决策能力转变。

image.png

image.png

image.png

image.png

3、高德地图与鸿蒙生态协同——华为鸿蒙生态负责人

双方在终端与云端协同、能力互通和开发者工具链上有深入合作,这对想覆盖鸿蒙和安卓的团队是重要机会。

image.png

image.png

image.png

image.png

四、颁奖环节

本次大赛给开发者设置了多个奖项,可以看到,非常丰厚。

  • 特等奖:奖励总额超 10 万元
  • 一等奖:奖励总额超 3 万元/组
  • 二等奖:奖励总额超 2 万元/组
  • 三等奖:奖励总额约 1 万元/组
  • 鸿蒙特别奖:奖励总额超 2 万元/组

奖项.png

2.png

虽然本次未获大奖,但收获颇丰,下次继续加油。

五、收获

个人收获可以概括为几点:

  • 技术层面:AI 与 LBS 的结合带来了更多落地场景,关键在意图识别、空间语义映射和 RAG 类检索的融合策略。
  • 产品层面:成功产品不是技术堆叠,而是将能力与用户真实需求对齐,先保证可用性和可靠性。
  • 工程实践:高质量的 POI 数据、持续标注和模型迭代是长期效果的保障;多模态融合要设计好降级策略以提高鲁棒性。
  • 生态与合作:开放能力与平台化能为创业团队加速,但也对架构和可扩展性提出更高要求。

六、总体体验

参赛感受:

这次大赛给我带来了充实且具体的收获——既有技术层面的启发,也有产品与工程实践的反思。与现场的团队和观众面对面交流,互相休息,让很多抽象的想法变得可讨论、可验证。

会场体验:

会场组织井然,展示与答辩区划分合理,现场有明确的打卡与咨询服务点,互动氛围很好。评委提问直接且具有针对性,能迅速把注意力拉回到产品价值与实现细节上,这对参赛者很有帮助。

行程体验:

高德官方为所有获奖开发者安排了住宿,并提供了全天餐饮,现场也有补给与礼品。大多数参赛者乘坐高铁或飞机到达,相关人员会提前沟通交通路线或报销信息,总体体验很棒。

最后特别感谢主办方与所有参赛开发者的辛勤付出。期待下一届能看到更多把 AI 与 LBS 更紧密结合、真正解决用户场景问题的项目。

image.png

经验分享2:SSR 项目中响应式组件的闪动陷阱与修复实践

背景

最近在开发公司官网的响应式 Banner 组件时,遇到了一个移动端首屏加载闪动的问题。 一套代码做PC端和移动端的适配,作为团队新人,我最初采用了 JS 动态判断的方案,但被 TL 指出存在性能问题。在这次代码 Review 中,我深刻理解了 SSR 项目中响应式适配的正确姿势。

一、组件设计:从 Props 接收到样式应用

1.1 组件设计思路

我们需要一个通用的 Banner 组件,支持:

  • PC 端和移动端使用不同的图片
  • PC 端和移动端使用不同的高度
  • 未提供移动端资源时,自动降级使用 PC 端资源

组件代码:

<script lang="ts" setup>

import { imgUrlPatch } from '~/util'

defineOptions({ name: 'HeroBanner' })

defineProps<{
  // 图像两侧不足时填充的背景色
  bgColor?: string
  // 最小高度
  height?: any
  // 图像网址
  url?: string
}>()
</script>
<template lang="pug">
.page-banner(
  :style="{ backgroundColor: bgColor, backgroundImage: url ? `url(${imgUrlPatch(url)})` : '', minHeight: height && (isFinite(height) ? height + 'px' : height) }"
)
  slot
</template>

<style lang="less">
.page-banner {
  background-position: center;
  background-repeat: no-repeat;
  background-size: auto 100%;
}
</style>

二、踩坑:首屏加载的高度闪动

2.1 问题复现

最初我在页面中这样使用组件:

<script lang="ts" setup>
const isMobileDevice = ref(false)

onMounted(() => {
  isMobileDevice.value = window.innerWidth < 768
})

const bannerHeight = computed(() => {
  return isMobileDevice.value ? '180px' : '360px'
})
</script>

<template>
  <HeroBanner 
    class="company-banner" 
    background-color="#2B5A8E" 
    :height="bannerHeight" 
    image="hero/company-banner.webp" 
  />
</template>

现象: 在移动端首次加载时,Banner 会出现明显的高度跳变(360px → 180px)。

2.2 TL 的修复方案

//父组件使用
<template>
  <HeroBanner 
    class="company-banner" 
    background-color="#2B5A8E" 
    :pc-height="360" 
    :mobile-height="180"
    pc-image="hero/company-banner.webp" 
  />
</template>

//子组件:HeroBanner 组件代码,背景图组件,自适应宽度,居中对齐
<script lang="ts" setup>
import { processImageUrl } from '~/utils'

defineOptions({ name: 'HeroBanner' })

const props = defineProps<{
  // 背景填充色
  backgroundColor?: string
  // PC 端高度
  pcHeight?: any
  // 移动端高度
  mobileHeight?: any
  // 移动端图片地址
  mobileImage?: string
  // PC 端图片地址
  pcImage?: string
}>()

// PC 端高度处理
const pcHeight = computed(() => {
  const h = props.pcHeight
  return h && (isFinite(h) ? h + 'px' : h)
})

// 移动端高度处理:优先使用 mobileHeight,否则降级使用 pcHeight
const mobileHeight = computed(() => {
  const h = props.mobileHeight || props.pcHeight
  return h && (isFinite(h) ? h + 'px' : h)
})

// 移动端图片:优先使用 mobileImage,否则降级使用 pcImage
const mobileImage = computed(() => {
  const img = props.mobileImage || props.pcImage
  return img ? `url(${processImageUrl(img)})` : ''
})

// PC 端图片
const pcImage = computed(() => {
  const img = props.pcImage
  return img ? `url(${processImageUrl(img)})` : ''
})
</script>

<template lang="pug">
.hero-banner(:style="{ backgroundColor }")
  slot
</template>

<style lang="less">
.hero-banner {
  background-color: v-bind(backgroundColor);
  background-image: v-bind(mobileImage);
  background-position: center;
  background-repeat: no-repeat;
  background-size: auto 100%;
  min-height: v-bind(mobileHeight);

  @media (min-width: 768px) {
    background-image: v-bind(pcImage);
    min-height: v-bind(pcHeight);
  }
}
</style>

删掉了所有 JS 判断逻辑,问题神奇地消失了。

三、原因深度剖析

3.1 错误方案的执行时序

我的方案(JS 动态判断):

1. SSR 服务端渲染
   └─ 服务端无法获取屏幕宽度
   └─ isMobileDevice 默认为 false
   └─ 输出 HTML: height="360px"

2. 浏览器接收 HTML
   └─ 用户看到 360px 高度的 Banner ⚠️

3. JavaScript 水合(Hydration)
   └─ onMounted 执行
   └─ window.innerWidth < 768true
   └─ isMobileDevice 变为 true
   └─ 触发响应式更新 → height="180px"
   └─ 用户看到高度跳变 ⚡

时间差: 从 HTML 渲染到 JS 执行完成,通常有 100-500ms 延迟。

3.2 正确方案的执行时序

TL 的方案(CSS 媒体查询):

1. SSR 服务端渲染
   └─ 同时输出两套高度值
   └─ 生成 CSS:
       min-height: 180px;  /* 默认 */
       @media (min-width: 768px) {
         min-height: 360px;  /* PC 端 */
       }

2. 浏览器接收 HTML
   └─ 浏览器原生解析 CSS
   └─ 媒体查询立即生效
   └─ 移动端直接显示 180px ✅
   └─ PC 端直接显示 360px ✅

3. JavaScript 水合
   └─ 无需任何操作
   └─ 样式已经正确 ✅

零时间差: CSS 在浏览器渲染引擎层面就已确定,不依赖 JS 执行。

3.3 核心差异对比表

对比维度 JS 动态判断(我的方案) CSS 媒体查询(TL 方案)
判断时机 JavaScript 执行后 浏览器解析 CSS 时
SSR 兼容 ❌ 服务端无法判断设备 ✅ 样式同时输出
首屏表现 需等待 hydration 立即应用正确样式
性能开销 有 JS 计算 + 响应式更新 浏览器原生能力
CLS 影响 有布局偏移 无布局偏移

四、经验总结

4.1 响应式适配的黄金法则

在 Nuxt/Vue SSR 项目中:

✅ 静态配置 → CSS 媒体查询
   - 固定高度、宽度
   - 静态资源路径
   - 固定颜色、字号

❌ 动态配置 → JS 运行时判断
   - API 返回的数据
   - 用户交互状态
   - 复杂业务逻辑

4.2 isFinite() 的实用技巧

遇到需要支持多种单位的场景,用原生 API 优雅处理:

function normalizeSize(value: any) {
  return value && (isFinite(value) ? `${value}px` : value)
}

// 使用
normalizeSize(100)      // '100px'
normalizeSize('50vh')   // '50vh'
normalizeSize('100%')   // '100%'
normalizeSize(null)     // null

4.3 组件设计的降级思维

提供 mobile*pc* 两套 props 时,始终实现降级逻辑:

const mobileValue = computed(() => {
  return props.mobileValue || props.pcValue  // 降级逻辑
})

这让组件使用更灵活,既支持"一套配置通用",也支持"精细化适配"。


五、延伸思考

5.1 什么时候必须用 JS 判断?

遇到以下场景,CSS 无法胜任,必须用 JS:

// ❌ CSS 无法实现:根据设备加载不同的组件
const DynamicComponent = computed(() => {
  return isMobile.value ? MobileChart : PCChart
})

// ❌ CSS 无法实现:根据屏幕尺寸调整 Swiper 配置
const swiperConfig = computed(() => ({
  slidesPerView: isMobile.value ? 1 : 3,
  spaceBetween: isMobile.value ? 10 : 30
}))

5.2 如何避免 SSR 水合不一致?

核心原则:服务端渲染的内容必须与客户端首次渲染一致

// ❌ 错误:服务端和客户端结果不一致
const currentTime = new Date().toLocaleString()

// ✅ 正确:仅在客户端执行
const currentTime = ref('')
onMounted(() => {
  currentTime.value = new Date().toLocaleString()
})

总结

这次代码 Review 让我深刻认识到:

  1. 性能优化不仅是算法,更是架构选择 - CSS 能做的事不要用 JS
  2. SSR 项目需要时序思维 - 区分服务端、客户端、水合三个阶段
  3. 组件设计要考虑降级 - 让开发者用得爽,而不是记一堆规则

感谢 TL 的耐心指导,以后写响应式组件会优先考虑 CSS 方案了!


参考资料:

首屏渲染中的hydration(水合)是现代前端框架(如React、Vue)在服务端渲染(SSR)或静态生成(SSG)中,将服务器生成的静态HTML内容激活为可交互应用的关键过程。

Hydration的核心作用是结合SSR和客户端渲染(CSR)的优势:

  • 服务器预先渲染完整的HTML并发送到浏览器,使用户快速看到内容,提升首次内容绘制(FCP)和SEO;
  • 随后客户端JavaScript下载并执行,通过对比虚拟DOM与现有真实DOM结构,将事件监听器和状态绑定到DOM元素上,使页面从静态视图变为可交互应用。

Hydration过程涉及以下关键步骤:

  1. 服务端渲染:服务器执行组件逻辑生成初始HTML和数据;
  2. 客户端激活:浏览器下载JavaScript bundle后,框架重新运行渲染逻辑生成虚拟DOM,与真实DOM对比并匹配结构,若一致则附加交互功能,不一致则触发hydration mismatch错误。

前端向架构突围系列 - 框架设计(三):用开闭原则拯救你的组件库

写在前面

兄弟们,回想一下,你有没有接过这种需求:

产品经理跑来说:“咱们那个通用的表格组件,现在需要在某一列加个自定义的渲染逻辑,以前是纯文本,现在要变成个带图标的按钮,还能点击弹窗。”

你心想:“这还不简单?”

于是你打开了那个祖传的 CommonTable.vueTable.tsx,找到了渲染单元格的地方,熟练地写下了一个 if-else

过了两天,产品又来了:“那啥,另一列也要改,这次要加个进度条。”

你又熟练地加了一个 else-if

几个月后,这个组件的源码已经突破了 2000 行,光那个 if-else 的判断逻辑就占了半屏。后来的同事接手时,看着这坨代码,只想把你拉黑。

这种“改哪哪疼,牵一发而动全身”的代码,就是典型的违反了开闭原则 (Open/Closed Principle, OCP) 。今天咱们就来聊聊,怎么用 OCP 把这坨代码重构成“人话”。


39072abf-1240-4203-a664-62f3074c67cd.png

什么是开闭原则 (OCP)?

开闭原则,听起来很高大上,其实说人话就是八个字:

对扩展开放,对修改关闭。

  • 对扩展开放 (Open for extension) :当有新需求来了,你应该能通过“增加新代码”的方式来满足,而不是去改旧代码。
  • 对修改关闭 (Closed for modification) :那个已经写好、测试过、稳定运行的核心代码,你尽量别去动它。

想象一下你的电脑主机。你想加个显卡,是直接把主板焊开接线(修改),还是找个 PCI-E 插槽插上去(扩展)?显然后者更靠谱。

在前端领域,OCP 最典型的应用场景就是组件设计插件系统


案例分析:一个“违反 OCP”的糟糕组件

咱们就拿最常见的通用列表项组件来举例。假设我们有一个 ListItem 组件,用来展示用户信息。

原始需求

需求很简单:展示用户的头像和名字。

// ListItem.tsx (V1)
interface User {
  id: string;
  name: string;
  avatar: string;
}

const ListItem = ({ user }: { user: User }) => {
  return (
    <div className="list-item">
      <img src={user.avatar} alt={user.name} />
      <span>{user.name}</span>
    </div>
  );
};

这代码看起来没毛病,清爽、简单。

需求变更 1:加个 VIP 标志

产品说:“有些用户是 VIP,名字后面得加个金灿灿的皇冠图标。”

你心想,小case,一把梭:

// ListItem.tsx (V2 - 开始变味了)
interface User {
  id: string;
  name: string;
  avatar: string;
  isVip?: boolean; // 新增字段
}

const ListItem = ({ user }: { user: User }) => {
  return (
    <div className="list-item">
      <img src={user.avatar} alt={user.name} />
      <span>{user.name}</span>
      {/* 修改点:硬编码逻辑 */}
      {user.isVip && <span className="vip-icon"></span>}
    </div>
  );
};

你为了这个新需求,修改ListItem 组件的内部实现。虽然只加了一行,但坏头已经开了。

需求变更 2:再加个在线状态

产品又来了:“得显示用户在不在线,在线的头像旁边亮个绿灯。”

你叹了口气,继续梭:

// ListItem.tsx (V3 - 味道越来越冲)
interface User {
  id: string;
  name: string;
  avatar: string;
  isVip?: boolean;
  isOnline?: boolean; // 又新增字段
}

const ListItem = ({ user }: { user: User }) => {
  return (
    <div className="list-item">
      <div className="avatar-wrapper">
        <img src={user.avatar} alt={user.name} />
        {/* 修改点:又硬编码逻辑 */}
        {user.isOnline && <span className="online-dot"></span>}
      </div>
      <span>{user.name}</span>
      {user.isVip && <span className="vip-icon"></span>}
    </div>
  );
};

问题来了:

  1. 组件越来越臃肿:每次新需求都要改这个文件,代码量蹭蹭涨。
  2. 耦合度极高ListItem 竟然要知道什么是 VIP,什么是在线状态。如果明天要加个“等级勋章”、“活动挂件”呢?
  3. 测试困难:每次改动都得把以前的 VIP、在线状态全测一遍,生怕改坏了。

这就是典型的违反了对修改关闭。核心组件被迫了解太多它不该知道的业务逻辑。


重构:用 OCP 把“屎山”铲平

怎么让 ListItem 既能支持各种花里胡哨的展示,又不用每次都改它呢?

答案就是:把变化的部分抽离出去,留下不变的骨架。

  • 不变的部分:列表项的基本结构(左边是图,右边是文字)。
  • 变化的部分:头像旁边要加什么装饰?文字后面要挂什么配件?

我们可以利用 React 的 组合 (Composition) 特性,比如 children 或者 Render Props(插槽槽位)。

重构 V1:使用插槽 (Slots / Render Props)

我们改造一下 ListItem,让它别管那么多闲事,只负责提供“坑位”。

// ListItem.tsx (OCP版本)
interface ListItemProps {
  avatar: React.ReactNode; // 不再只传字符串,直接传节点
  title: React.ReactNode;  // 同上
  // 预留两个扩展槽位
  avatarAddon?: React.ReactNode;
  titleAddon?: React.ReactNode;
}

// 这个组件现在稳定得一批,几乎不需要再修改了
const ListItem = ({ avatar, title, avatarAddon, titleAddon }: ListItemProps) => {
  return (
    <div className="list-item">
      <div className="avatar-wrapper">
        {avatar}
        {/* 扩展点:头像装饰 */}
        {avatarAddon}
      </div>
      <div className="title-wrapper">
        {title}
        {/* 扩展点:标题装饰 */}
        {titleAddon}
      </div>
    </div>
  );
};

现在,核心组件 ListItem 对修改是关闭的。那怎么扩展新需求呢?

在使用它的地方进行扩展(对扩展开放):

// UserList.tsx (业务层)
import ListItem from './ListItem';

const UserList = ({ users }) => {
  return (
    <div>
      {users.map(user => (
        <ListItem
          key={user.id}
          // 基础信息
          avatar={<img src={user.avatar} />}
          title={<span>{user.name}</span>}
          // 扩展需求1:在线状态
          avatarAddon={user.isOnline ? <OnlineDot /> : null}
          // 扩展需求2:VIP标识
          titleAddon={user.isVip ? <VipCrown /> : null}
        />
      ))}
    </div>
  );
};

看!世界清静了。

  • ListItem 组件不知道也不关心什么是 VIP。它只知道:“如果有人给了我 titleAddon,那我就把它渲染在标题后面。”
  • 如果明天产品要加个“等级勋章”,你只需要写个 <LevelBadge /> 组件,然后传给 titleAddon 即可。ListItem.tsx 文件一个字都不用改。

这就是 OCP 的魅力。


进阶:策略模式与配置化

在更复杂的场景下,比如我们开头提到的通用表格组件,每一列的渲染逻辑可能千奇百怪。这时候光用插槽可能还不够灵活。

我们可以借鉴策略模式的思想,结合配置化来实现 OCP。

假设我们有一个复杂的后台管理表格。

糟糕的设计 (违反 OCP)

// BadTableColumn.tsx
const renderCell = (value, columnType) => {
  // 地狱 if-else 
  if (columnType === 'text') {
    return <span>{value}</span>;
  } else if (columnType === 'image') {
    return <img src={value} />;
  } else if (columnType === 'link') {
    // ...要加新类型就得改这里
  } else if (columnType === 'status') {
     // ...越来越长
  }
  // ...
};

符合 OCP 的设计

我们定义一个策略注册表,把每种类型的渲染逻辑注册进去。

// renderStrategies.tsx (策略定义)
const strategies = {
  text: (value) => <span>{value}</span>,
  image: (value) => <img src={value} className="table-img" />,
  // 新需求:状态标签
  status: (value) => <Tag color={value === 'active' ? 'green' : 'red'}>{value}</Tag>,
};

// 提供注册入口(对扩展开放)
export const registerStrategy = (type, renderer) => {
  strategies[type] = renderer;
};

// 提供获取入口
export const getStrategy = (type) => {
  return strategies[type] || strategies['text'];
};

然后,表格组件只负责调用策略:

// GoodTableColumn.tsx
import { getStrategy } from './renderStrategies';

const TableCell = ({ value, columnType }) => {
  // 核心组件对修改关闭:它不需要知道具体怎么渲染
  const renderer = getStrategy(columnType);
  return <td>{renderer(value)}</td>;
};

当你要新增一种“进度条”类型的列时,你根本不需要碰 TableCell 组件,只需要在项目的入口文件里注册一个新的策略:

// main.js (应用入口)
import { registerStrategy } from './renderStrategies';
import ProgressBar from './components/ProgressBar';

// 扩展新能力
registerStrategy('progress', (value) => <ProgressBar percent={value} />);

这就实现了一个简易的插件化系统。核心库稳定不变,业务方通过注册机制无限扩展能力。


总结:别让自己成为“改Bug机器”

开闭原则不是什么高深的理论,它就是为了让你少加班、少背锅而生的。

记住这几个实战要点:

  1. 识别变化点:做组件之前先想想,哪些是铁打不动的骨架,哪些是流水易变的皮肉。
  2. 多用组合/插槽:React 的 children 和 Render Props,Vue 的 slot,都是实现 OCP 的利器。把决定权交给使用者,而不是自己大包大揽。
  3. 善用策略/配置:遇到复杂的 if-else 逻辑判断渲染类型时,考虑用映射表(Map 对象)代替硬编码,把逻辑抽离出去。

下次再遇到产品经理不断提新需求,希望你能自信地打开代码,优雅地新增一个文件,而不是痛苦地在那坨几千行的祖传代码里加 if-else

Keep coding, keep open!


互动话题

你的项目里有没有那种因为违反 OCP 而变得维护困难的“超级组件”?你又是怎么重构它的?欢迎在评论区吐槽交流!

前端向架构突围系列 - 框架设计(二):糟糕的代码有哪些特点?

前言 你有没有过这种经历:新接手了一个项目,产品经理让你把一个按钮往左移 5 像素。你心想:“这不有手就行?” 结果你改了 CSS,保存,刷新。 按钮是移过去了,但登录弹窗打不开了,控制台红了一片,甚至 CI/CD 流程都挂了。

这一刻,你面对的不是代码,而是一座摇摇欲坠的屎山。 在框架设计和组件库开发中,这种现象尤为致命。业务代码写烂了,坑的是一个页面;框架设计写烂了,坑的是整个团队。今天我们要聊的不是具体的变量命名或缩进,而是架构层面的“设计臭味”

image.png

什么是“设计臭味”?

“代码臭味”(Code Smell)这个词不是说代码真的有味儿(虽然有时候看代码确实想吐),而是指代码结构中某些特征暗示了深层次的设计问题

它就像煤气泄漏的味道,本身不一定会炸,但只要有一点火星(新的需求变更),整个系统就会原地升天。

作为前端架构师或核心开发者,如果你在 Code Review 时闻到了以下这 5 种味道,请务必警惕。


1. 僵化性 (Rigidity):牵一发而动全身

症状: 你想复用一个通用的 Header 组件,结果发现它里面硬编码了 useRouter() 的跳转逻辑,甚至还直接 importRedux/Pinia 的 store。 你想在另一个项目用它?没门。除非你把那边的路由和状态管理全套搬过来。

前端实战翻译: 这就是典型的高耦合。组件不再是一个独立的乐高积木,而是一块焊死在主板上的芯片。

反面教材 (React):

// 这是一个充满僵化味道的组件
const UserProfile = () => {
  // 致命伤1:直接依赖具体的全局状态
  const user = useSelector(state => state.user.info);
  // 致命伤2:直接依赖具体的路由实现
  const history = useHistory();

  const handleLogout = () => {
     // 业务逻辑耦合在UI里
     api.logout().then(() => history.push('/login'));
  }

  return <div>{user.name} <button onClick={handleLogout}>退出</button></div>;
};

指南:

  • 控制反转 (IoC) :组件只管展示,逻辑通过 Props 传进来。
  • Presentational vs Container:把“展示组件”和“容器组件”拆开。展示组件要像“傻瓜”一样,给什么吃什么,不要自己去冰箱(Store)里拿。

2. 脆弱性 (Fragility):改东崩西的蝴蝶效应

症状: 这比僵化性更搞心态。僵化性是你改不动,脆弱性是你改了,但崩在了你完全想不到的地方。 比如:你为了优化首页加载速度,调整了一个公共 utils 函数,结果结算页面的金额计算错了,多给了用户 100 块钱。

前端实战翻译: 通常源于隐式依赖全局变量污染或者CSS 样式穿透

反面教材 (CSS/Vue):

/* 这种写法在全局样式里简直是灾难 */
.title {
  font-size: 20px;
  color: red;
}

/* 或者在组件里滥用 !important */
.btn {
  background: blue !important; /* 你的同事想覆盖这个样式时,必须写得比你更恶心 */
}

指南:

  • CSS Modules / Scoped CSS / Tailwind:坚决消灭全局样式冲突。
  • 纯函数 (Pure Functions) :工具类函数坚决不能有副作用,输入相同,输出必须相同。
  • 依赖显式化:别在组件里偷偷摸摸读 window.xxx 或者 localStorage,把它们封装成 Hooks 或服务。

3. 顽固性 (Immobility):无法拆分的连体婴

症状: 你写了一个非常炫酷的 DataGrid 表格,支持排序、筛选、分页。隔壁组看到了说:“哇,这个好,我也要用。” 你自信满满地把代码发给他。 五分钟后他跑来说:“哥,我只要个表格UI,你为什么把 Axios 拦截器、ElementUI 的弹窗组件、甚至你们公司的埋点 SDK 都打包进来了?”

前端实战翻译: 这是内聚性低的表现。业务逻辑和基础设施、UI 逻辑混在一起,导致根本无法拆分复用。

反面教材:

JavaScript

// 一个原本想做通用组件的 hook,却混入了业务
function useTableData(url) {
  const [data, setData] = useState([]);

  useEffect(() => {
    // 错误:这里耦合了特定的 HTTP 库和业务上的 token 逻辑
    axios.get(url, { headers: { 'X-Auth': localStorage.getItem('token') } })
      .then(res => setData(res.data.list)); // 错误:硬编码了数据结构 res.data.list
  }, [url]);

  return data;
}

指南:

  • Headless UI:这是现在的设计趋势(如 React Table, TanStack Query)。只提供逻辑钩子,不提供 UI。
  • 依赖倒置:网络请求层应该作为参数传入,而不是在组件内部直接实例化。

4. 粘滞性 (Viscosity):做错误的事更容易

症状: 这是一个人性问题。 假设你的框架支持 TypeScript。

  • 正确的做法:定义 Interface,继承 Props,处理泛型,写 Mock 数据,跑单元测试。需要 10 分钟。
  • 错误的做法any 一把梭。需要 10 秒钟。

做正确的事做错误的事阻力大得多时,开发者就会倾向于破坏架构。这就是粘滞性。

前端实战翻译: 环境配置太复杂、类型定义太反人类、测试难写。

反面教材: 如果你的组件库要求使用者必须写 5 层嵌套的配置对象才能跑起来,那使用者一定会想办法绕过配置,直接去改源码。

指南:

  • 约定优于配置:像 Next.js 或 Nuxt.js 那样,文件放对位置路由就自动生成了。
  • 提供开箱即用的类型:别让用户自己去猜泛型填什么。
  • 路径依赖设计:让最简单的写法,就是最佳实践。

5. 晦涩性 (Opacity) & 过度设计 (Needless Complexity)

症状: 这两个往往相伴而生。 你打开一个同事的代码,看到了一堆 AbstractFactoryProviderHighOrderComponentWrapper。 你只是想渲染一个输入框,结果你需要先创建一个 FormConfig,再实例化一个 FieldBuilder,最后通过 RenderProp 传进去。

开发者看着你的代码会感叹:“虽然看不懂,但感觉很厉害的样子。” —— 别傻了,他们心里在骂娘。

前端实战翻译: 为了封装而封装。比如把简单的 if-else 逻辑抽象成极其复杂的策略模式,或者写了无比抽象的 Hooks,结果参数传了 8 个,返回值 12 个。

指南:

  • YAGNI 原则 (You Ain't Gonna Need It):不要为你臆想的未来需求写代码。
  • 代码如文章:好的代码应该像大白话一样。如果一段代码需要你写 10 行注释来解释“我为什么要绕这么大弯子”,那通常意味着设计失败。
  • 组合优于继承,简单优于抽象:在前端,特别是 React Hooks 中,平铺直叙的逻辑往往比层层嵌套的高阶组件要好维护得多。

总结:如何避免成为“制造臭味”的人?

设计框架就像盖楼。

  • 僵化性是钢筋没绑好,想改户型得拆承重墙。
  • 脆弱性是地基没打牢,楼上装修楼下漏水。
  • 顽固性是水电管线混在一起,修电线得砸水管。
  • 粘滞性是垃圾道设计不合理,大家只好往楼下扔垃圾。

要去除这些“味道”,最核心的心法只有一句话:

保持代码的“软”度 (Software)。

软件之所以叫软件,是因为它应该是易于改变的。当我们写下一行代码时,多问自己一句: “如果明天这个需求变了,我今天写的这行代码是资产,还是债务?”


互动话题

你的项目里有没有那种“甚至不敢看它一眼,怕看一眼它就崩了”的代码?或者你见过最离谱的“过度设计”是什么样的?欢迎在评论区晒出你的“受苦”经历,让大家开心一下(划掉)避避坑。

React基础框架搭建10-webpack配置:react+router+redux+axios+Tailwind+webpack

webpack配置

npm install --save-dev webpack webpack-cli webpack-dev-server
npm install --save-dev babel-loader @babel/core @babel/preset-env @babel/preset-react
npm install --save-dev html-webpack-plugin clean-webpack-plugin
npm install --save-dev css-loader style-loader
npm install --save-dev file-loader url-loader
npm install --save-dev mini-css-extract-plugin
npm install --save-dev dotenv-webpack

在根目录创建webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const Dotenv = require('dotenv-webpack');

module.exports = {
    mode: 'development', // 开发模式
    entry: './src/index.js', // 入口文件
    output: {
        path: path.resolve(__dirname, 'dist'), // 输出目录
        filename: 'bundle.js', // 输出文件名
        publicPath: '/', // 公共路径
    },
    resolve: {
        extensions: ['.js', '.jsx'], // 解析的文件扩展名
        alias: {
            '@': path.resolve(__dirname, 'src'), // 设置路径别名
        },
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/, // 处理 JavaScript 和 JSX 文件
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env', '@babel/preset-react'], // Babel 配置
                    },
                },
            },
            {
                test: /\.css$/, // 处理 CSS 文件
                use: ['style-loader', 'css-loader'],
            },
            {
                test: /\.(png|jpg|gif|svg)$/, // 处理图片文件
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[path][name].[ext]', // 保持原有路径和文件名
                        },
                    },
                ],
            },
        ],
    },
    devServer: {
        static: {
            directory: path.join(__dirname, 'dist'), // 更新为 static
        },
        compress: true, // 启用 gzip 压缩
        port: 3000, // 端口号
        historyApiFallback: true, // 支持 HTML5 History API
    },
    plugins: [
        new CleanWebpackPlugin(), // 清理输出目录
        new HtmlWebpackPlugin({
            template: './public/index.html', // HTML 模板
            filename: 'index.html', // 输出的 HTML 文件名
        }),
    ],
};

在 package.json 中添加 Webpack 的构建和开发脚本:

"scripts": {
    "start": "webpack serve --open", // 启动开发服务器
    "build": "webpack --mode production" // 构建生产版本
}

前端向架构突围系列 - 架构方法(三):前端设计文档的写作模式

Gemini_Generated_Image_awm4poawm4poawm4.png

引言:架构师的“身后事”

  • 痛点直击:你是否遇到过这种情况?新项目启动时写了几十页 文档 (打个比方) ,三个月后代码改得面目全非,文档却再也没人打开过。
  • 核心冲突:代码是写给机器看的(追求准确),文档是写给未来的团队看的(追求理解)。
  • 观点抛出:一份好的架构文档(ADD - Architecture Design Document)不是繁文缛节,它是团队协作的“契约”和技术决策的“黑匣子”。

1. 法则一:升维思考 —— 引入 C4 模型

上一篇我们讲了 UML(微观的画笔),这一篇我们要讲宏观的地图。

  • 为什么要引入 C4?

    • 传统的 4+1 视图对敏捷开发来说有时过于厚重。
    • C4 模型 (Context, Containers, Components, Code) 像 Google Maps 一样,提供了从“全球”到“街道”的 4 个缩放级别,更适合现代 Web 应用。
  • 前端视角的 C4 拆解(图文结合):

    • Level 1: System Context (系统上下文)
    • 画什么:你的 Web App 和外部系统(支付网关、老后端、CDN)的关系。
    • 给谁看:非技术人员、产品经理。
    graph TD
      A[电商前端] -->|HTTPS| B[支付网关]
      A -->|WebSocket| C[实时推荐服务]
      D[老后端] -->|REST| A
      E[CDN] -->|静态资源| A
    
    • Level 2: Containers (容器)

      • 画什么这是前端架构师的主战场。SPA 客户端、BFF 层 (Node.js)、微前端子应用、移动端 WebView。
      • 给谁看:开发团队、运维。
    graph TB
      subgraph 前端架构
        A[SPA客户端] -->|GraphQL| B[BFF层]
        B -->|RPC| C[微前端子应用]
        D[移动端WebView] -->|REST| B
      end
    
    • Level 3: Components (组件)

      • 画什么:不是 React UI 组件,是业务功能模块(如:购物车控制器、身份认证服务、日志模块)。
    graph LR
      A[购物车控制器] -->|事件总线| B[库存校验服务]
      A -->|LocalStorage| C[本地缓存]
      D[身份认证模块] -->|JWT| E[API网关]
    
    • Level 4: Code (代码)

      • 观点不要画! 代码变化太快,图通过 IDE 自动生成即可,画了必死。

2. 法则二:决策留痕 —— 架构决策记录 (ADR)

这是本文最“硬核”、最能体现架构师价值的部分。

  • 什么是 ADR?

    • 文档不仅要记录“由于什么(Result)”,更要记录“为什么(Why)”。
    • 很多时候,新同事骂代码烂,是因为他们不知道当年的约束条件
  • ADR 标准模板(直接给干货):

    • 标题:ADR-001 采用 Tailwind CSS 替代 Styled-components

    • 状态:已通过 / 已废弃 / 提议中

    • 背景 (Context) :现有 CSS-in-JS 方案导致 Bundle 体积过大,且团队命名困难。

    • 决策 (Decision) :全线引入 Tailwind CSS。

    • 后果 (Consequences)

      • (+) 这里的 HTML 会变丑。
      • (+) 样式文件体积减少 40%。
      • (-) 需要统一配置 ESLint 插件进行类名排序。
  • 建议

    • 不要把 ADR 写在 Word 里,要放在代码仓库的 /docs/adr 目录下。

3. 法则三:文档即代码 (Docs as Code)

如何保证文档“活着”?让它和代码住在一起。

  • 工具链推荐

    • 存储:Git 仓库(与 package.json 同级)。
    • 编写:Markdown。
    • 画图:Mermaid(直接在 Markdown 里写代码生成图,便于 Git Diff 对比修改)。
    • 发布:VitePress / Docusaurus(自动构建成静态站点)。
  • 目录结构示例

4. 落地模板:一份“不被嫌弃”的架构文档骨架

  1. 背景与目标 (1句话说明项目价值)

    • [痛点/现状] + [解决方案] + [量化价值]
    • 示例 : “打造企业级前端微服务基座,通过微前端架构解耦巨石应用,实现多团队独立部署,并统一全线产品的 UI 交互体验与鉴权逻辑。”

    image.png

  2. 约束条件 (必须兼容 IE?必须 2 周上线?)

    • 根据内部自定义
  3. 系统架构图 (示例)

    graph LR
    %% 重构类项目
    P1["老旧系统"] --> 
    S1["技术升级<br/>React 18 + Vite"] --> 
    V1["性能提升<br/>维护成本降低"]
    
    %% 高并发项目
    P2["CSR瓶颈"] --> 
    S2["SSR渲染<br/>CDN分发"] --> 
    V2["SEO优化<br/>转化率提升"]
    
    %% 中台项目
    P3["巨石应用"] --> 
    S3["微前端<br/>模块解耦"] --> 
    V3["独立部署<br/>团队协作优化"]
    
    %% 样式
    classDef pStyle fill:#ffebee,stroke:#f44336,color:#b71c1c
    classDef sStyle fill:#e3f2fd,stroke:#2196f3,color:#0d47a1
    classDef vStyle fill:#e8f5e9,stroke:#4caf50,color:#1b5e20
    
    class P1,P2,P3 pStyle
    class S1,S2,S3 sStyle
    class V1,V2,V3 vStyle
    
  4. 关键技术选型 (链接到具体的 ADR 文件)

    graph LR
    %% 技术分类
    框架选型 --> React
    构建工具 --> Vite
    渲染模式 --> NextJS
    架构方案 --> Qiankun
    
    %% 具体技术
    React["React 18"] --> ADR1[" ADR-001"]
    Vite["Vite 5.x"] --> ADR2["ADR-002"]
    NextJS[" Next.js SSR"] --> ADR3[" ADR-003"]
    Qiankun[" qiankun"] --> ADR4[" ADR-004"]
    
    %% 替代说明
    React -.->|替代 jQuery| Legacy1
    Vite -.->|替代 Webpack| Legacy2
    NextJS -.->|优化首屏| Target1["FCP < 1.5s"]
    Qiankun -.->|微前端基座| Target2["模块解耦"]
    
    %% 样式
    classDef cat fill:#f5f5f5,stroke:#616161,font-weight:bold
    classDef tech fill:#bbdefb,stroke:#1976d2,color:#0d47a1
    classDef adr fill:#c8e6c9,stroke:#388e3c,color:#1b5e20
    
    class 框架选型,构建工具,渲染模式,架构方案 cat
    class React,Vite,NextJS,Qiankun tech
    class ADR1,ADR2,ADR3,ADR4 adr
    
  5. 非功能性需求 (NFRs)

    • 性能:FCP < 1.5s
    • 安全:XSS 防护策略
    • 监控:Sentry 报警规则
    graph LR
        %% 根节点
        NFRs["非功能性需求"] --> Perf
        NFRs --> Sec
        NFRs --> Mon
    
        %% 性能需求
        Perf["性能需求<br/>Performance"] --> P1["首屏<1.5s"]
        Perf --> P2["交互<100ms"]
        Perf --> P3["资源优化"]
    
        %% 安全需求
        Sec["安全需求<br/>Security"] --> S1["XSS防护"]
        Sec --> S2["CSRF防护"]
        Sec --> S3["数据加密"]
    
        %% 监控需求
        Mon["监控需求<br/>Monitoring"] --> M1["错误率<0.1%"]
        Mon --> M2["性能告警"]
        Mon --> M3["用户追踪"]
    
        %% 样式
        classDef rootStyle fill:#34495e,stroke:#2c3e50,color:white,font-size:16px
        classDef perfStyle fill:#3498db,stroke:#2980b9,color:white
        classDef secStyle fill:#e74c3c,stroke:#c0392b,color:white
        classDef monStyle fill:#2ecc71,stroke:#27ae60,color:white
    
        class NFRs rootStyle
        class Perf,P1,P2,P3 perfStyle
        class Sec,S1,S2,S3 secStyle
        class Mon,M1,M2,M3 monStyle
    

概括总结

“架构师的产出不是文档,而是共识。”

好的架构文档,不是为了证明你通过了答辩,而是为了让新入职的同学在一年后看到这行代码时,能通过文档里的 ADR 明白: “哦,原来当初是为了性能才写得这么‘奇怪’的。”

这就是文档的价值——穿越时间的沟通。

前端向架构突围系列 - 架构方法(一):概述 4+1 视图模型

Gemini_Generated_Image_wtlcmdwtlcmdwtlc.png

这个模型由 Philippe Kruchten 在 1995 年提出。它的本质含义是:没有一种单一的视图能够涵盖系统的方方面面。不同的利益相关者(Stakeholders)关心的是不同的东西。

  • 业务方关心功能(能不能用?)。
  • 开发关心代码结构(好不好写?)。
  • 运维关心部署和硬件(稳不稳定?)。
  • 用户关心操作流程(顺不顺畅?)。

架构师的职责,就是通过这 5 个视角,把这些“鸡同鸭讲”的需求统一成一个完整的系统设计。

为了让你更好理解,我将这个经典的后端/通用架构概念,完整“翻译”成前端架构师的视角


1. 场景视图 (Scenarios / Use Cases View) —— “+1” 的那个核心

本质:系统的灵魂,它驱动了其他 4 个视图。 这是架构设计的起点。如果不知道系统要干什么,设计就无从谈起。

  • 关注点:用户怎么用这个系统?核心业务流程是什么?

  • 谁看:所有利益相关者(产品经理、测试、开发、用户)。

  • 前端架构视角

    • 这不是指某个具体的 Button 点击事件,而是关键链路 (Critical User Journeys)
    • 例子:用户进入首页 -> 登录 -> 浏览商品 -> 加入购物车 -> 结算。
    • 架构决策:如果“秒杀”是核心场景,那么你在后续的“处理视图”中就必须设计高并发的防抖策略;在“物理视图”中就要考虑 CDN 缓存。
graph LR
    %% 样式定义
    classDef icon fill:#fff9c4,stroke:#fbc02d,stroke-width:3px,color:#333,rx:50,ry:50;
    classDef step fill:#fff,stroke:#fbc02d,stroke-width:2px,color:#333,rx:10,ry:10;
    classDef note fill:#fffde7,stroke:none,color:#666;

    %% 左侧:核心概念
    User((用户<br/>User)):::icon

    %% 右侧:关键链路 (Critical Journey)
    subgraph Journey [关键链路: 秒杀场景]
        direction LR
        Step1[进入详情页]:::step --> Step2[抢购点击]:::step
        Step2 --> Step3[排队等待]:::step
        Step3 --> Step4[创建订单]:::step
        Step4 --> Step5[支付成功]:::step
    end

    User ==> Step1

    %% 架构决策点
    Note1(架构决策点:<br/>CDN缓存, 骨架屏):::note -.-> Step1
    Note2(架构决策点:<br/>高并发防抖, 乐观UI):::note -.-> Step2

2. 逻辑视图 (Logical View) —— “功能是怎么组织的?”

本质:系统的抽象模型。 这是最接近业务逻辑的一层,忽略具体的代码文件,只看概念

  • 关注点:系统有哪些“部件”?它们之间是什么关系?

  • 谁看:开发人员、业务分析师。

  • 前端架构视角

    • 组件模型:原子组件 vs 业务组件。
    • 领域模型:User, Product, Order 等实体定义(TypeScript Interface 定义)。
    • 状态管理设计:全局状态(Redux/Pinia)存什么?局部状态存什么?模块间如何通信?
    • 例子:画一张图,展示 OrderList 组件依赖 UserStoreAPI Service,而不关心它们具体写在哪个文件里。
graph LR
    %% 样式定义
    classDef icon fill:#e1f5fe,stroke:#039be5,stroke-width:3px,color:#333,rx:50,ry:50;
    classDef layer fill:#fff,stroke:#039be5,stroke-width:2px,color:#333,rx:5,ry:5;
    classDef rel stroke:#90caf9,stroke-width:2px,stroke-dasharray: 5 5;

    %% 左侧
    Logic((抽象<br/>逻辑)):::icon

    %% 右侧:分层架构
    subgraph LayerSystem [前端逻辑分层]
        direction TB
        UI[<b>表现层 UI Layer</b><br/>Button, Layout, Page]:::layer
        Adapter[<b>适配层 Adapter</b><br/>Hooks, Presenters]:::layer
        Domain[<b>领域层 Domain</b><br/>UserEntity, CartModel]:::layer
        Infra[<b>基础层 Infra</b><br/>Axios, Storage, Logger]:::layer
    end

    Logic ==> UI

    %% 依赖关系 (单向依赖是架构的关键)
    UI --> Adapter
    Adapter --> Domain
    Adapter --> Infra

3. 开发视图 (Development / Implementation View) —— “代码是怎么写的?”

本质:系统的静态组织结构。 这是程序员每天面对的 IDE 里的样子。

  • 关注点:文件目录怎么分?用什么框架?依赖怎么管?

  • 谁看:开发人员、构建工程师。

  • 前端架构视角

    • 工程化结构:Monorepo (Nx/Turborepo) 还是 Multirepo?
    • 目录规范src/components, src/hooks, src/utils 怎么归类?
    • 依赖管理package.json 里的依赖,公共库(Shared Library)如何抽取?
    • 构建工具:Vite/Webpack 配置,分包策略(Chunking)。
    • 例子:决定把所有的 API 请求封装在 @api 目录下,并禁止组件直接调用 axios,这就是开发视图的约束。
graph LR
    %% 样式定义
    classDef icon fill:#f3e5f5,stroke:#8e24aa,stroke-width:3px,color:#333,rx:50,ry:50;
    classDef file fill:#fff,stroke:#8e24aa,stroke-width:2px,color:#333,rx:2,ry:2;
    classDef tool fill:#f3e5f5,stroke:#8e24aa,stroke-width:1px,color:#333,stroke-dasharray: 5 5;

    %% 左侧
    Dev((工程<br/>代码)):::icon

    %% 右侧:目录与工具
    subgraph ProjectStructure [工程化与目录规范]
        direction TB
        
        subgraph Mono [Monorepo 仓库]
            Pkg1[packages/ui-lib]:::file
            Pkg2[apps/web-client]:::file
            Config[tsconfig.json]:::file
        end

        subgraph Toolchain [构建工具链]
            Vite(Vite / Webpack):::tool
            Lint(ESLint / Prettier):::tool
        end
    end

    Dev ==> Mono
    Mono -.-> Toolchain

4. 处理视图 (Process View) —— “系统是怎么运行的?”

本质:系统的动态行为、并发与性能。 对于前端来说,这是最容易被忽视,但最考验功底的一层。

  • 关注点:性能、并发、同步/异步、时序。

  • 谁看:系统集成人员、高级开发。

  • 前端架构视角

    • 异步流控:接口竞态问题(Race Condition)怎么处理?Promise 并发限制。
    • 生命周期:SSR(服务端渲染)的数据注水(Hydration)流程是怎样的?
    • 性能优化:Web Worker 处理复杂计算,避免阻塞主线程(UI 线程)。
    • 通信机制:WebSocket 怎么保持心跳?跨 Tab 通信(SharedWorker/LocalStorage)怎么做?
    • 例子:设计一个“大文件分片上传”的功能,你需要画出切片、上传、暂停、续传的时序图,这属于处理视图。

graph LR
    %% 样式定义
    classDef icon fill:#e8f5e9,stroke:#43a047,stroke-width:3px,color:#333,rx:50,ry:50;
    classDef action fill:#fff,stroke:#43a047,stroke-width:2px,color:#333,rx:10,ry:10;
    classDef async fill:#c8e6c9,stroke:none,color:#333,rx:5,ry:5;

    %% 左侧
    Run((运行<br/>时序)):::icon

    %% 右侧:大文件上传时序
    subgraph AsyncProcess [大文件分片上传流程]
        direction TB
        Start[开始上传]:::action --> Check{检查文件}:::action
        Check -->|太大| Slice[Web Worker<br/>进行切片计算]:::async
        Slice --> Upload[并发上传切片<br/>Promise.all]:::action
        Upload --> Pause{网络中断?}:::action
        Pause -->|是| Wait[暂停 & 记录断点]:::async
        Pause -->|否| Finish[合并请求]:::action
    end

    Run ==> Start

5. 物理视图 (Physical / Deployment View) —— “代码跑在哪里?”

本质:软件到硬件的映射。 前端代码最终是要通过网络传输并运行在用户设备上的。

  • 关注点:部署、网络拓扑、硬件限制。

  • 谁看:运维工程师 (DevOps)、系统管理员。

  • 前端架构视角

    • 部署策略:静态资源上 CDN,Nginx 反向代理配置。
    • 运行环境:BFF 层运行在 Docker 容器里;前端代码运行在用户的 Chrome/Safari 里(考虑兼容性)。
    • 网络环境:弱网情况下如何降级?离线包(PWA)策略。
    • 多端适配:同一套代码是跑在 PC 浏览器,还是内嵌在 App 的 WebView 里?
    • 例子:决定使用“灰度发布”系统,将新版本的 JS 文件只推给 10% 的用户,这属于物理视图的范畴。
graph LR
    %% 样式定义
    classDef icon fill:#fff3e0,stroke:#fb8c00,stroke-width:3px,color:#333,rx:50,ry:50;
    classDef device fill:#fff,stroke:#fb8c00,stroke-width:2px,color:#333,rx:5,ry:5;
    classDef net fill:#ffe0b2,stroke:none,color:#333,rx:20,ry:20;

    %% 左侧
    Deploy((部署<br/>环境)):::icon

    %% 右侧:部署拓扑
    subgraph NetworkTopology [资源分发与运行环境]
        direction LR
        
        subgraph Cloud [云端设施]
            CICD[CI/CD 构建产物]:::device --> OSS[对象存储]:::device
            OSS --> CDN((CDN 边缘节点)):::net
        end

        subgraph Client [用户终端]
            CDN --> Browser[PC 浏览器]:::device
            CDN --> Mobile[手机 WebView]:::device
            CDN --> Hybrid[小程序]:::device
        end
    end

    Deploy ==> Cloud

总结:软件开发的本质是什么?

通过 4+1 视图,我们可以得出软件开发的本质:

  1. 控制复杂度 (Managing Complexity) : 如果没有分层和视图,系统就是一团乱麻。4+1 试图把复杂问题拆解成 5 个维度分别解决。

  2. 沟通与妥协 (Communication & Trade-offs) : 架构不是追求“完美的代码”,而是平衡各方利益。

    • 为了物理视图的加载速度(上 CDN),可能要牺牲开发视图的便利性(复杂的构建流程)。
    • 为了处理视图的流畅度(虚拟列表),可能要增加逻辑视图的复杂度。

此时此刻,你可以做的 Next Step

作为想转型的架构师,不要只写代码,开始写文档

你可以挑选你当前负责的一个复杂模块,尝试写一份简易版的技术设计文档 (TDD) ,强制自己包含以下三点:

  1. 逻辑图:用方块图画出组件和数据流。
  2. 处理图:用时序图画出关键的用户交互流程。
  3. 部署说明:说明代码构建后怎么发布,有没有缓存策略。

年薪 50W 的前端,到底比年薪 15W 的强在哪里?

65ef63f6bd30ab838939a4ae_Developer productivity tools 2024.webp

昨天我看新年第一波简历 看破防了

最近团队缺人,我连着看了一周的简历。

说实话,看得我挺难受的。😖

我发现一个特别普遍的现象:很多工作了四五年的兄弟,期望薪资填个 25k 甚至 30k,但你仔细翻他的项目经历,全是后台管理系统,全是 H5 拼图页面,全是表单增删改查。

你问他:这几年你遇到的最大技术难点是啥?🤔

他回你:表单字段太多了,校验逻辑太复杂。或者说,产品经理改需求太频繁。😖

听到这种回答,我心里大概就有了底:这兄弟的薪资上限,大概率锁死在 20W 以内了。

这就是咱们常说的 CRUD 困局。

你会 Vue,你会 React,你会用 Antd 画页面,你会调接口。兄弟,这些在 2018 年也许能让你拿高薪,但现在是 2026 年了,这些东西是基建,是培训班出来的应届生两个月就能上手的。🤣

那么问题来了,那个坐在你隔壁工位、平时话不多、但年薪能拿 50W 的大佬,他到底比你强在哪?

是他敲键盘比你快?还是他发量比你少?

都不是。

我觉得最核心的差距,就只有三点。听我细说。


你在做填空,他在设计整张试卷

web-development-programmer-engineering-coding-website-augmented-reality-interface-screens-developer-project-engineer-programming-software-application-design-cartoon-illustration_107791-3863.avif

这事儿特别明显。就拿新开一个项目来说。

15W 的兄弟是怎么干的?

找个脚手架,create-react-app 一把梭。然后开始堆页面,写组件。遇到要用的工具函数?去百度搜一个粘贴进来。遇到样式冲突?加个 !important 搞定。代码格式乱了?不管了,先跑通再说。

他的脑子里只有一个字:做。

50W 的兄弟是怎么干的?

他在写第一行业务代码之前,会先在脑子里过一遍这几件事:

大家代码风格不一样怎么办?先把 ESLint + Prettier + Husky 这一套流水线配好,谁提交的代码格式不对,连 git push 都推不上去。

这个项目以后会不会变大?要不要直接上 Monorepo 管理?

公共组件怎么抽离?是不是该搭个私有 npm 库?

打包速度怎么优化?Vite 的配置能不能再调调?

这就是差距。🤔

老板愿意给他 50W,不是因为他页面画得快,而是因为他制定了标准。他一个人,能让团队剩下 10 个人的产出质量变高。这叫工程化视野,这才是值钱的玩意儿。


出了事,你只会甩锅,他能兜底

software-developer-vs-software-engineer-illustration.jpg

场景再具体点:用户投诉页面卡顿,加载慢。

15W 的兄弟通常反应是这样的:

打开控制台 Network 看一眼。

哎呀,接口这就 800ms 了,这后端不行啊,锅在服务端。

嗨🙂‍↔️,这图片 UI 给得太大了,切图没切好。

这数据量几万条,浏览器渲染本来就慢,我也没办法!

总之,只要不是 JS 报错,这事儿就跟我没关系。

50W 的兄弟会干嘛?

他不会废话,他直接打开 Chrome 的 Performance 面板,像做外科手术一样分析。

这一段掉帧,是不是触发了强制重排?

内存这一路飙升,是不是哪个闭包没释放,或者 DOM 节点没销毁?

主线程卡死,是不是长任务阻塞了渲染?能不能开个 Web Worker 把计算挪出去?

网络慢,是不是 HTTP/2 的多路复用没吃满?关键资源的加载优先级设对了吗?

这就叫底层能力。🤔

平时写业务看不出来,一旦遇到高并发、大数据量、若网环境这种极端场景,只会调 API 的人两手一摊,而懂底层原理的人能从浏览器内核里抠出性能。

这种 兜底能力,就是你的溢价。


他是业务合伙人!

How-to-become-a-Backend-Developer.jpg

这点最扎心。

产品经理提了个不靠谱的需求,比如要在手机端展示一个几百列的超级大表格。

15W 的兄弟:

心里骂娘:这傻X产品,脑子有坑。😡🤬

嘴上老实:行吧,我尽量试试。

结果做出来卡得要死,体验极差,上线被用户骂,回来接着改,陷入无尽加班。

这种思维模式下,你就是个执行资源,也就是个 打工人。

50W 的兄弟:

他听完需求直接就怼回去了:

哥们,在手机上看几百列表格,用户眼睛不要了?你这个需求的业务目标是啥?是为了让用户核对数据?

如果是核对数据,那我们要不要换个方案,只展示关键指标,点击再下钻看详情?这样开发成本低了 80%,用户体验还好。

这就叫技术变现。

高端的前端,不仅仅是写代码的,他是懂技术的业务专家。他能用技术方案去纠正产品逻辑,帮公司省钱,帮业务赚钱。

在老板眼里,你是成本,他是投资。🤷‍♂️


哪怕现在是 15W,咱也能翻盘

如果你看上面这些话觉得膝盖中了一箭,别慌。谁还不是从切图仔过来的?

想打破这个 CRUD 的怪圈,从明天上班开始,试着变一下:

别再只盯着那几个 API 了

Vue 文档背得再熟也就是个熟练工。去看看源码,看看人家是怎么设计响应式的,看看 React 为什么要搞 Fiber。懂了原理,你就不怕框架变。

别做重复工作

下次想复制粘贴工具函数的时候,停一下。试着自己封装一个通用的,甚至试着把你们项目里重复的逻辑抽成一个库。工程化就是这么一点点做起来的。

钻进去一个细分领域

别啥都学,啥都学不精。

可视化、低代码、Node.js 中间件、音视频,随便挑一个,把它钻透。在任何一个细分领域做到前 5%,你都有议价权。


还是那句话!前端并没有死,死的是那些 只会切图和调接口 的工具人。

50W 的年薪,买的不是你的时间,而是你 解决复杂问题 的能力,和你 避免团队踩坑 的经验。

别再满足于重复做一个 CRUD 了。下次打开编辑器的时候,多问自己一句:

除了把这个功能做出来,我还能为这段代码多做点什么?

共勉🙌

Suggestion (2).gif

React基础框架搭建7-测试:react+router+redux+axios+Tailwind+webpack

现在可以对之前弄得一些内容进行测试了

npm下载:

npm install --save-dev @testing-library/react @testing-library/jest-dom

有些可能需要安装:

npm install --save-dev @babel/plugin-proposal-private-property-in-object

单元测试请按如下方式进行:

//src/views/Home/__tests__/Home.test.js

import React from 'react';
import { render, screen } from '@testing-library/react';
import Home from '../Home';

test('renders welcome message', () => {
    render(<Home />);
    const linkElement = screen.getByText(/Hello/i);
    expect(linkElement).toBeInTheDocument();
});

运行:

npm test
❌