阅读视图

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

我接手了一个 10 年前的 jQuery 老项目,用 Web Components 给它续了命

3680358_4d2e_2.webp

前几个月,主管把我拉进一个小黑屋,语重心长地说:那个 2015 年上线的 CRM 系统,客户想加个AI 智能客服的功能,虽然是个老项目,但这是公司的大金矿,你来负责呗!😃。

我打开代码仓库的那一刻,两眼一黑。

  • jQuery 1.8.3,古董级的版本。
  • 没有 Webpack,没有 Vite,只有无尽的 <script> 标签。
  • 全局变量漫天飞,一个 common.js 有 8000 行,里面充斥着 varfunction
  • CSS 样式全剧透,随便改个 div 的 padding,隔壁页面的布局就崩了。

这时候,我的脑子里有两个小人在打架:

  • 全删了!用 Vue3 + Vite 重构!这代码是人写的吗?😖
  • 别冲动!这项目逻辑极其复杂,重构就是火葬场,上线出了 Bug 你负责?

现实是残酷的:业务只给了 3 天时间,重构是不可能的😒。

但在满屏 $('#id').show() 的代码里写现代化的 AI 对话界面?光是解决 CSS 样式冲突就能让我加班到猝死。

最后,我没有选择 React,也没用 iframe(通信太麻烦),而是掏出了浏览器原生的大杀器——Web Components

结果?我只用了一个 JS 文件,就完美地把现代 UI种进了这坨老代码里,而且没引发任何副作用。

今天就来聊聊,这套屎山续命指南


为什么我选择了 Web Components?

1_NE0TVSyDZ2MC7ngfGYa7gg.jpg

在老项目里加新功能,最大的痛点是什么?

不是 JS 逻辑混乱,是 CSS 污染。

老项目里通常会有这种霸道的全局样式:

/* style.css */
div { box-sizing: content-box; }
input { border: 1px solid red; } /* 不知道哪位前辈留下的坑 */
.btn { float: left; }

如果你直接引入一个 Vue/React 组件,这些全局样式会瞬间摧毁你的 UI。

Web Components 的 Shadow DOM(影子 DOM) ,就是为此而生的。

它创造了一个完全隔离的 DOM 作用域

  • 外部的 CSS 进不来(你的组件不会乱)。
  • 内部的 CSS 出不去(你不会搞崩老页面)。

在 jQuery 页面里开始植入AI 助手

假设我们不需要任何构建工具(Webpack/Vite),直接在老页面的 HTML 里写。

定义组件

不用引入任何库,直接用原生 ES6 Class。

// ai-assistant.js
class AiAssistant extends HTMLElement {
  constructor() {
    super();
    // 1. 开启 Shadow DOM,这是隔离的关键
    this.shadow = this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    // 2. 渲染 UI 和 独立的 CSS
    this.render();
    // 3. 绑定事件
    this.shadow.querySelector('#send-btn').addEventListener('click', () => {
      this.handleSend();
    });
  }

  render() {
    this.shadow.innerHTML = `
      <style>
        /* 这里的样式绝对安全,不会影响外部,也不受外部影响 */
        :host {
          position: fixed;
          bottom: 20px;
          right: 20px;
          z-index: 9999;
        }
        .container {
          background: #fff;
          border-radius: 12px;
          box-shadow: 0 4px 20px rgba(0,0,0,0.15);
          width: 300px;
          font-family: 'Segoe UI', sans-serif; /* 我们可以用现代字体 */
        }
        .header { background: #007bff; color: white; padding: 10px; border-radius: 12px 12px 0 0; }
        .content { height: 200px; padding: 10px; overflow-y: auto; color: #333; }
        input { width: 70%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
        button { background: #007bff; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; }
      </style>

      <div class="container">
        <div class="header">🤖 AI 助手</div>
        <div class="content" id="chat-box">
          <div>你好,我是你的智能助手,有什么可以帮您?</div>
        </div>
        <div style="padding: 10px; border-top: 1px solid #eee;">
          <input type="text" id="input-msg" placeholder="输入问题..." />
          <button id="send-btn">发送</button>
        </div>
      </div>
    `;
  }

  handleSend() {
    const input = this.shadow.querySelector('#input-msg');
    const text = input.value;
    if (!text) return;

    // 模拟添加消息
    const chatBox = this.shadow.querySelector('#chat-box');
    chatBox.innerHTML += `<div style="text-align:right; margin:5px 0; color: #007bff;">${text}</div>`;
    input.value = '';

    // 关键:如何跟外部 jQuery 通信?抛出原生 CustomEvent
    this.dispatchEvent(new CustomEvent('new-question', {
      detail: { question: text },
      bubbles: true,
      composed: true // 允许穿透 Shadow DOM 冒泡出去
    }));
  }
}

// 注册组件
customElements.define('ai-assistant', AiAssistant);

在 jQuery 老页面中使用

只需要引入 JS,然后像写 <div> 一样写标签。

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="legacy-style.css">
  <script src="jquery-1.8.3.min.js"></script>
</head>
<body>
  <div class="old-header">...</div>
  
  <ai-assistant></ai-assistant>

  <script src="ai-assistant.js"></script>

  <script>
    // jQuery 监听组件抛出的事件
    $(document).ready(function() {
      // 这里的事件监听依然丝滑
      $('ai-assistant').on('new-question', function(e) {
        // 注意:jQuery 的 event 对象封装了一层,原生的 detail 在 originalEvent 里
        var question = e.originalEvent.detail.question;
        console.log("老页面收到了 AI 提问:", question);
        
        // 这里可以调用老系统的 API
        // $.ajax(...) 
      });
    });
  </script>
</body>
</html>

为什么说这是屎山的终极解法?

侵入性为零

我在 ai-assistant.js 里写了 input { width: 70% }

如果是普通的 Vue/React 组件,这行样式可能会把老页面所有的 input 都搞乱。

但因为有 Shadow DOM,它只对自己生效。老页面的 style.css 就算写了 div { display: none },也影响不到我 Shadow Root 里的布局。

技术栈框架无关

Web Components 是浏览器原生标准。

这意味着,无论这坨屎山是基于 jQuery、AngularJS 1.x、PHP 模板还是 JSP,只要它是浏览器,它就能跑这个组件

未来如果你们终于决定重构,换成了 React 19,这个 <ai-assistant> 标签依然可以直接复制过去用,不用改一行代码。

渐进式迁移

我们可以不用搞重构。

今天把用户卡片改成 Web Component。

明天把侧边栏改成 Web Component。

这是一种 岛屿架构 (Astro 类似) 。我们在屎山💩的海洋里,建立一个个小岛。等岛屿连成一片,屎山自然就消失了。


避坑指南(干货)

虽然很香,但也有几个坑要注意:

  1. React 兼容性: 如果你的老项目里混用了 React 16/17,它们对 Web Components 的事件绑定支持不太好(React 19 已完美修复)。
  2. 字体图标: Shadow DOM 里的字体图标(如 FontAwesome)如果定义在外部 CSS 里,内部读不到。需要在 Shadow DOM 的 <style>@import 进来,或者用 <link> 引入。
  3. 构建工具: 如果组件逻辑复杂,还是建议用 LitStencil 这种轻量库来写,然后打包成一个 bundle.js 丢给老项目引用,开发体验会好很多(支持 TS、响应式数据)。

面对老旧项目,我们往往有两种极端的态度:要么摆烂(接着堆屎),要么激进点(推倒重来)。

但作为成熟的工程师,我们更需要中间平滑路线。Web Components 恰恰给了我们非常好的解决方案。

下次再遇到老板让你给 10 年前的 JSP 页面加功能,别急着提离职。

试着建一个 <my-feature>,你会发现,屎山其实也没那么可怕😁。

大家平时怎么解决老古董项目呢?🤔

谢谢大家.gif

❌