Web Components:被React“统治”多年后,我决定试试原生组件化

一台会思考的电脑
2026-01-15 06:31
阅读 1654

大家好,我是小林,一个双非学校的大二学生。别看我还在读大二,其实我已经在现在的前端组干了快两年了——从高中毕业那年暑假开始实习,到现在几乎成了“老人”。我们团队不大,但节奏贼快,上周五晚上又因为产品经理临时改需求加班到十一点,运维还在群里@我说“别把测试环境搞崩了”,我只能苦笑。

我一直是个底层原理爱好者,喜欢扒一扒浏览器到底怎么渲染、JS引擎怎么执行。虽然现在 React 几乎成了前端标配,但最近我越来越觉得,有些场景下它有点“杀鸡用牛刀”了。比如上个月,我们接到一个任务:给公司内部的多个老系统(有 jQuery 的、有 Vue 1 的、甚至还有纯原生 JS 的)统一嵌入一个数据采集模块,用于抓取页面上的关键信息——说白了就是个轻量级爬虫组件,要能跨框架复用。

这时候,React 就显得有点“重”了。你总不能让每个老系统都装一套 React 运行时吧?包体积、兼容性、依赖冲突……想想就头大。于是,我翻出了尘封已久的 Web Components 文档,决定亲自试试这个“原生组件化”的新(其实也不新)方案。

为什么是 Web Components?

Web Components 是一套由 W3C 标准定义的浏览器原生技术,包含四个核心部分:

  • Custom Elements:自定义 HTML 标签
  • Shadow DOM:封装样式和 DOM,避免污染
  • HTML Templates:声明式模板
  • ES Modules:模块化加载

最大的优势?不用任何框架,浏览器原生支持! 现代浏览器(Chrome、Edge、Firefox、Safari)基本都支持了,连 IE 都不用考虑(感谢公司去年彻底淘汰了 IE)。

更重要的是,它天生跨框架。你可以在 React 项目里用,在 Vue 里用,甚至在纯静态 HTML 里直接 <my-crawler></my-crawler> 插入,完全无感。

动手写一个“爬虫组件”

我们的需求很简单:用户点击按钮,组件自动扫描页面中符合特定 class 的元素(比如 .price, .title),提取文本并上报。

先看最终使用方式:

<!-- 在任何页面里,只要引入这个组件 -->
<script type="module" src="./crawler-component.js"></script>

<page-crawler 
  target-classes="price,title,description"
  report-url="https://api.example.com/collect"
></page-crawler>

是不是清爽?不需要 npm install,不需要 build,直接 script 引入就能用。

第一步:定义 Custom Element

// crawler-component.js
class PageCrawler extends HTMLElement {
  constructor() {
    super();
    
    // 创建 Shadow DOM,隔离样式
    this.attachShadow({ mode: 'open' });
    
    // 初始化配置
    this.targetClasses = this.getAttribute('target-classes')?.split(',') || [];
    this.reportUrl = this.getAttribute('report-url') || '';
  }

  connectedCallback() {
    // 组件挂载时创建按钮
    this.render();
    this.bindEvents();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        .crawler-btn {
          padding: 8px 16px;
          background: #4CAF50;
          color: white;
          border: none;
          border-radius: 4px;
          cursor: pointer;
          font-size: 14px;
        }
        .crawler-btn:hover {
          background: #45a049;
        }
      </style>
      <button class="crawler-btn">开始采集</button>
    `;
  }

  bindEvents() {
    const button = this.shadowRoot.querySelector('.crawler-btn');
    button.addEventListener('click', () => {
      this.scrapeAndReport();
    });
  }

  scrapeAndReport() {
    const data = {};
    
    this.targetClasses.forEach(cls => {
      const elements = document.querySelectorAll(`.${cls.trim()}`);
      data[cls] = Array.from(elements).map(el => el.innerText.trim());
    });

    // 模拟上报
    fetch(this.reportUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    })
    .then(() => {
      alert('采集成功!');
    })
    .catch(err => {
      console.error('上报失败:', err);
      alert('采集失败,请检查网络');
    });
  }
}

// 注册自定义元素
customElements.define('page-crawler', PageCrawler);

踩坑记录

  1. Shadow DOM 里的事件怎么冒泡?
    最初我以为 Shadow DOM 完全隔离,结果发现 click 事件其实会穿透到主文档。但如果你在 Shadow 内部用了 stopPropagation(),外部就收不到了。这点要小心,不过对我们这种独立组件影响不大。

  2. 属性更新没反应?
    Web Components 的属性默认不会自动响应变化。如果需要动态修改 target-classes,得实现 attributeChangedCallback 并声明 observedAttributes

    static get observedAttributes() {
      return ['target-classes', 'report-url'];
    }
    
  3. IE?不存在的
    公司早就放弃 IE 了,所以 polyfill 也没加。但如果你的用户还在用 IE,得用 webcomponents/polyfills。不过说实话,2024 年还支持 IE 的项目,建议直接重构。

和 React 组件对比:谁更香?

我知道很多人会问:既然我们组主要用 React,为啥不用 React 写个组件然后用 ReactDOM.render 挂载到老系统里?

理论上可行,但实际很麻烦。比如:

  • 每个老系统都要引入 React + ReactDOM(~40KB gzipped)
  • 如果老系统本身有 React,版本冲突怎么办?
  • 构建流程复杂,得单独打包一份 UMD 格式的 bundle
  • 调试困难,React DevTools 在老系统里可能不生效

而 Web Components:

  • 无依赖,纯原生
  • 体积小(上面那个组件压缩后不到 2KB)
  • 开箱即用,一行 script 搞定
  • 调试直接看 Elements 面板,Shadow DOM 也能展开

为了验证,我做了个简单对比:

特性 Web Components React 组件
体积 ~2KB ~40KB+
跨框架 ✅ 原生支持 ❌ 需额外处理
样式隔离 ✅ Shadow DOM ❌ 需 CSS Modules / styled-components
学习成本 中(需理解 Shadow DOM) 低(团队熟悉)
开发体验 一般(无热更新) 优秀(HMR、DevTools)
浏览器支持 现代浏览器✅ 依赖 Babel/构建

结论:简单、独立、跨框架的 UI 功能,Web Components 更合适;复杂交互、状态管理强的,还是 React 香。

实战:在 React 项目里用 Web Components

有意思的是,Web Components 也能在 React 里用!虽然 React 对自定义元素的支持有点“别扭”(比如属性要用 camelCasekebab-case),但配合一点封装,完全可行。

// React 中使用
import './crawler-component.js'; // 引入

function App() {
  return (
    <div>
      <h1>我的 React 应用</h1>
      {/* 注意:React 不识别 kebab-case 属性,得用 dataset 或 ref */}
      <page-crawler 
        ref={el => {
          if (el) {
            el.setAttribute('target-classes', 'price,title');
            el.setAttribute('report-url', 'https://api.example.com/collect');
          }
        }}
      />
    </div>
  );
}

或者更优雅的方式:用一个 wrapper 组件:

const CrawlerWrapper = ({ targetClasses, reportUrl }) => {
  const ref = useRef(null);
  
  useEffect(() => {
    if (ref.current) {
      ref.current.setAttribute('target-classes', targetClasses.join(','));
      ref.current.setAttribute('report-url', reportUrl);
    }
  }, [targetClasses, reportUrl]);

  return <page-crawler ref={ref} />;
};

这样,同一个组件,既能用在 React 项目,也能用在 jQuery 项目,真正做到“一次编写,到处运行”。

性能和兼容性

我们在测试环境跑了性能对比:

  • 首次加载时间:Web Components 快 300ms(因为没 React 初始化)
  • 内存占用:Web Components 低约 15%
  • 交互响应:几乎无差别

兼容性方面,Can I Use 显示:

  • Chrome 67+ ✅
  • Firefox 63+ ✅
  • Safari 10.1+ ✅
  • Edge 79+ ✅

国内主流浏览器(基于 Chromium)基本没问题。唯一要注意的是,某些国产浏览器的“极速模式”可能有问题,但我们的内部系统只允许用 Chrome,所以无忧。

总结:小而美的解决方案

这次实践让我对 Web Components 刮目相看。它不是要取代 React,而是填补了轻量级、跨框架、原生集成的空白。

尤其适合以下场景:

  • 内部工具组件(如埋点、监控、爬虫)
  • 微前端中的子应用共享 UI
  • 第三方 SDK(比如客服插件、支付按钮)
  • 老系统渐进式改造

作为双非出身、靠自学走到现在的人,我特别喜欢这种“回归原生”的思路。不需要花里胡哨的框架,用浏览器自带的能力解决问题,既高效又可控。

当然,Web Components 也有短板:开发体验不如 React/Vue、生态弱、调试工具少。但对特定场景,它真的是“刚刚好”。

最后,分享一句我常对自己说的:“不要为了用新技术而用,要为了解决问题而用。” 这次,Web Components 解决了我们的痛点,就够了。

下周,我打算用它重构公司的统一登录浮层。要是成功了,再来分享!


P.S. 今天又是 8 点起床写代码的一天。虽然双非,但代码不会骗人。加油,打工人!

评论 0

最热最新
暂无评论
一台会思考的电脑Lv.1
0
影响力
0
文章
0
粉丝