我接手了一个 10 年前的 jQuery 老项目,用 Web Components 给它续了命
前几个月,主管把我拉进一个小黑屋,语重心长地说:那个 2015 年上线的 CRM 系统,客户想加个AI 智能客服的功能,虽然是个老项目,但这是公司的大金矿,你来负责呗!😃。
我打开代码仓库的那一刻,两眼一黑。
- jQuery 1.8.3,古董级的版本。
-
没有 Webpack,没有 Vite,只有无尽的
<script>标签。 -
全局变量漫天飞,一个
common.js有 8000 行,里面充斥着var和function。 -
CSS 样式全剧透,随便改个
div的 padding,隔壁页面的布局就崩了。
这时候,我的脑子里有两个小人在打架:
- 全删了!用 Vue3 + Vite 重构!这代码是人写的吗?😖
- 别冲动!这项目逻辑极其复杂,重构就是火葬场,上线出了 Bug 你负责?
现实是残酷的:业务只给了 3 天时间,重构是不可能的😒。
但在满屏 $('#id').show() 的代码里写现代化的 AI 对话界面?光是解决 CSS 样式冲突就能让我加班到猝死。
最后,我没有选择 React,也没用 iframe(通信太麻烦),而是掏出了浏览器原生的大杀器——Web Components。
结果?我只用了一个 JS 文件,就完美地把现代 UI种进了这坨老代码里,而且没引发任何副作用。
今天就来聊聊,这套屎山续命指南。
为什么我选择了 Web Components?
在老项目里加新功能,最大的痛点是什么?
不是 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 类似) 。我们在屎山💩的海洋里,建立一个个小岛。等岛屿连成一片,屎山自然就消失了。
避坑指南(干货)
虽然很香,但也有几个坑要注意:
- React 兼容性: 如果你的老项目里混用了 React 16/17,它们对 Web Components 的事件绑定支持不太好(React 19 已完美修复)。
-
字体图标: Shadow DOM 里的字体图标(如 FontAwesome)如果定义在外部 CSS 里,内部读不到。需要在 Shadow DOM 的
<style>里@import进来,或者用<link>引入。 -
构建工具: 如果组件逻辑复杂,还是建议用 Lit 或 Stencil 这种轻量库来写,然后打包成一个
bundle.js丢给老项目引用,开发体验会好很多(支持 TS、响应式数据)。
面对老旧项目,我们往往有两种极端的态度:要么摆烂(接着堆屎),要么激进点(推倒重来)。
但作为成熟的工程师,我们更需要中间平滑路线。Web Components 恰恰给了我们非常好的解决方案。
下次再遇到老板让你给 10 年前的 JSP 页面加功能,别急着提离职。
试着建一个 <my-feature>,你会发现,屎山其实也没那么可怕😁。
大家平时怎么解决老古董项目呢?🤔