Web Components:原生组件化开发新趋势

VSCode信徒
2025-12-15 00:13
阅读 592

早上8点,成都的阳光刚爬上窗台,我一边泡着茶一边盯着 CI/CD 流水线里飘红的构建记录——又是前端打包失败。作为团队里那个“啥都得管一点”的 DevOps 工程师,我其实早就习惯了这种日常:React 项目升级依赖炸了、微前端子应用加载异常、测试环境资源争抢……但上周五晚上那场事故,真让我重新思考起“组件化”这件事。

事情是这样的:我们正在搞一个内部运维平台重构,产品经理希望把监控面板、部署流水线、日志查询这些模块做成可复用的“卡片”,不同团队能自由组合。原本打算直接套 React + Storybook 搞一套 Design System,结果 QA 在 Safari 上测出样式隔离失效,Chrome 下 Shadow DOM 被 React 强行穿透,更别提 IE11(别笑,有些客户还在用)。当时看着控制台满屏的 Uncaught TypeError: Cannot read property 'shadowRoot' of null,我真的想砸键盘。

为什么又回到“原生”?

说起来有点打脸。作为早年 Vue 和 React 双修的老前端,我一直觉得框架才是正道。但这次需求有个硬性约束:必须支持跨技术栈嵌入。我们的监控系统是 Angular 写的,部署平台是 Vue3,而新运维后台是 React 18 —— 总不能让每个团队都为了一个按钮去装整套 React 运行时吧?Webpack 打包体积分分钟爆炸。

这时候 Web Components 这个“古董”突然闪进脑海。其实它早在 2014 年就进了 W3C 标准,但一直被框架生态压着打。不过最近两年,随着 Lit、Stencil 这些轻量库的成熟,加上浏览器原生支持越来越好(连 Edge 都全面拥抱了),它居然悄悄成了跨框架组件化的“银弹”。

安全意识插播:Web Components 的 Shadow DOM 天然提供样式和 DOM 隔离,这对多租户 SaaS 系统特别友好——再也不用担心某个团队写了个全局 .btn { margin: -9999px } 把整个平台 UI 干趴。

实战踩坑:从“Hello World”到线上跑

我拿运维平台里的“部署状态指示器”当试验田。需求很简单:传入 pipeline ID,自动拉取状态并高亮显示。用 Web Components 实现核心逻辑如下:

class DeploymentStatus extends HTMLElement {
  constructor() {
    super();
    // 创建 Shadow DOM,开启样式隔离
    this.attachShadow({ mode: 'open' });
  }

  static get observedAttributes() {
    return ['pipeline-id'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'pipeline-id' && newValue) {
      this.fetchStatus(newValue);
    }
  }

  async fetchStatus(id) {
    // 注意:这里要处理 CORS 和认证!别直接裸奔 fetch
    const res = await fetch(`/api/v1/pipelines/${id}/status`, {
      headers: { 'Authorization': `Bearer ${this.getAuthToken()}` }
    });
    const data = await res.json();
    this.render(data.status);
  }

  render(status) {
    this.shadowRoot.innerHTML = `
      <style>
        .status { padding: 8px; border-radius: 4px; }
        .running { background: #e3f2fd; color: #1976d2; }
        .success { background: #e8f5e9; color: #388e3c; }
        .failed { background: #ffebee; color: #d32f2f; }
      </style>
      <div class="status ${status}">${status.toUpperCase()}</div>
    `;
  }
}

customElements.define('deployment-status', DeploymentStatus);

关键点来了:

  1. 属性监听:通过 observedAttributes 监听 pipeline-id 变化,比 React 的 props 更底层但更灵活
  2. 安全边界:所有样式都在 Shadow DOM 内,外部 CSS 完全无法污染
  3. 无运行时依赖:直接 <script> 引入就能用,Angular/Vue/React 里直接当 HTML 标签写

但坑也不少。比如在 React 里动态更新属性时,得用 ref 操作 DOM 而非 setState:

// React 中正确更新 Web Component 属性的方式
const StatusCard = ({ pipelineId }) => {
  const statusRef = useRef(null);

  useEffect(() => {
    if (statusRef.current) {
      statusRef.current.setAttribute('pipeline-id', pipelineId);
    }
  }, [pipelineId]);

  return <deployment-status ref={statusRef} />;
};

要是直接写成 <deployment-status pipeline-id={pipelineId} />,React 会把 pipeline-id 当成普通 prop 而非 HTML attribute,组件根本收不到更新!

性能与兼容性:现实很骨感

上线前我做了份对比测试(数据基于 MacBook Pro M1):

方案 首屏 JS 体积 渲染 100 个组件耗时 Safari 兼容性
React Functional 120 KB 240 ms 完美
Web Components (原生) 0 KB 180 ms iOS 10.3+
Web Components (Lit) 8 KB 150 ms iOS 10.3+

Lit 库虽然只加了 8KB,但提供了响应式更新、模板绑定等糖,开发体验接近现代框架。我们最终选了它,毕竟没人想手写 innerHTML 拼字符串(XSS 风险警告⚠️)。

至于 IE11… 对不起,我们默默在文档里加了句“建议使用现代浏览器”。运维同事吐槽:“你们前端总说兼容,结果自己先放弃了。” 我回他:“要不你来维护 IE 的 Polyfill?” 他立刻闭嘴了。

代码人生的反思:框架 vs 原生

写这篇文章时,窗外玉林路的小酒馆还没开张。回想自己从 jQuery 时代一路卷到 React Hooks,现在又回头研究原生 API,有点像极了人生——兜兜转转,才发现有些“老东西”之所以没死,是因为真香。

Web Components 不是万能的。复杂交互场景(比如带状态管理的表单)还是 React 更顺手;需要服务端渲染?Next.js 它不香吗?但当你面临跨技术栈复用极致轻量化强隔离需求时,这个被遗忘的原生方案反而成了最优解。

上周双11大促,运维平台稳定跑了 72 小时,那个小小的部署状态指示器在 Angular 监控页和 React 后台里同时闪烁绿光。那一刻我觉得,所谓 DevOps,不就是让不同技术栈的人能安心睡觉嘛。

最后送大家一句我在团队 Slack 里常说的话:“不要为了用新技术而用,但永远别关上尝试新可能的门。” 尤其当产品经理又提“这个功能下周上线”的时候——谁知道 Web Components 会不会救你狗命呢?

P.S. 如果你也在成都,欢迎约茶聊技术(别聊 IE 兼容)。运维人的代码人生,需要多点烟火气。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝