
如果你做过几年前端一定经历过这种场景新项目启动先选框架再配组件库然后封装自己的公共组件等到换个团队或者换个项目之前的组件全部作废只能重新造轮子。组件化喊了这么多年其实我们一直被“框架绑架”你写的组件离开那个框架就是一堆废代码。这就是 Web Components 想要解决的根本问题——用浏览器原生标准让组件真正成为“积木”而不是某套框架的附属品。Web Components 不是什么新东西但这两年明显感觉到热度在回升一方面是现代浏览器兼容性终于到位了另一方面是大家都在反思框架过重、长期维护成本太高的问题。这篇文章会从浏览器底层工作原理讲起把 Custom Elements、Shadow DOM、Template、Slot 这几个核心概念拆开揉碎再用一个真实可跑的组件案例演示“搭积木”的完整过程最后分享一些我在实际项目中积累的避坑经验。无论你是想全面理解浏览器渲染机制还是想找一个组件复用问题的原生解法这篇文章应该都能给你一些参考。1. 组件化的困境为什么我们需要浏览器原生方案1.1 前端组件化简史从 MVC 到框架组件前端开发在很长一段时间里是没有什么“组件”概念的。最早是 jQuery 时代每个人都在写“面条式”的 DOM 操作项目大了就变成一笔糊涂账。后来出现了 Backbone、AngularJS我们开始有了 MVC 的概念但严格来说那只是把数据、视图、逻辑分层组件与组件之间并没有清晰的边界。到了 React 和 Vue 这些现代框架出现组件化才真正成为主流。它们提出了“组件就是一个函数/对象输入属性输出 UI”的理念加上 props、state、事件、生命周期这些东西确实让前端开发像搭积木了。但问题也随之而来这套积木是绑定在某一个厂商的标准之上的。你在 React 里写了一个“日期选择器”组件想拿到 Vue 项目里直接用答案是不行。要么重写要么通过一套适配层去 bridge麻烦得很。而且每当框架更新换代组件库就要跟着升级你的公司在某个框架上沉淀了几年的组件资产说废弃就废弃这种沉没成本相当痛苦。1.2 Web Components 给出了怎样的答案Web Components 是一组浏览器原生 API 的统称它定义了一套不依赖任何框架的组件标准。这就像所有积木厂商终于坐下来商量好了统一的接口尺寸不管哪家生产的积木只要符合这个标准就能拼到一起。它由四块核心组成Custom Elements自定义元素允许开发者注册自己的 HTML 标签例如product-card。Shadow DOM影子 DOM为组件提供独立的 DOM 树和样式隔离空间让组件内部的样式和结构不泄漏到外部。HTML TemplateHTML 模板通过template标签定义可复用的内容片段浏览器不会渲染它只在需要时实例化。ES ModulesES 模块提供标准的模块化加载方式让组件可以用import引入。这四块各自有明确分工Custom Elements 负责“组件是什么”Shadow DOM 负责“组件内部的样子”Template 负责“组件的内容模板”ES Modules 负责“组件怎么被组装起来”。合在一起你就可以像搭积木一样把一个个独立、自洽、可复用的 UI 部件拼成整个应用。1.3 浏览器原生 API 的“香”与“不香”香的地方在于不需要引入任何运行时所有现代浏览器原生支持性能开销几乎为零组件跨框架通用React 项目里能用Vue 项目里能用甚至原生 JS 项目里也能用没有框架升级的迁移成本因为它是标准的一部分。不香的地方也很明显原生 API 的写法比较啰嗦模板、样式、逻辑都散落着没有像 Vue 单文件组件那样的语法糖没有状态管理、路由这些配套设施响应式系统需要自己造轮子。所以实际项目中我一般不会建议把所有东西都用 Web Components 重写更推荐的做法是——在框架项目里把一些跨项目复用的基础组件抽成 Web Components或者在新项目里完全基于 Web Components 搭建配合一个轻量的状态管理库。2. 核心概念深度拆解Custom Elements 与 Shadow DOM2.1 Custom Elements浏览器如何识别一个新标签当你写下product-card这样一个标签时浏览器最初并不认识它。它只是一个未知元素跟div没有任何区别。但当你调用customElements.define(product-card, ProductCard)之后浏览器就会把这个标签和一个类绑定起来后续遇到这个标签就会自动实例化对应的类。这里隐藏了浏览器的一个核心机制自定义元素存在“升级”upgrade的过程。即使你的自定义元素类定义得晚但 HTML 解析器已经把product-card解释成了一个 HTMLElement也没什么问题浏览器会在define调用之后立即把页面上已存在的所有product-card元素从“未知元素”升级为“自定义元素”触发它的构造函数和连接回调。这也是为什么你把 script 放在body底部也可以正常工作。Custom Elements 的生命周期回调是理解这个机制的关键constructor()组件实例创建时调用适合初始化状态、绑定事件但不能在这里访问 DOM 或添加子节点因为元素还没被连接。connectedCallback()组件被挂载到 DOM 时调用适合做数据获取、DOM 渲染等操作。disconnectedCallback()组件从 DOM 移除时调用适合清理定时器、解绑全局事件。attributeChangedCallback(name, oldValue, newValue)当你在observedAttributes里声明的属性发生变化时调用适合根据属性变化更新视图。adoptedCallback()组件被移动到新的文档时调用用的场景不多但比如 iframe 间移动元素时会触发。常见的新手坑是很多人直接在constructor里操作 DOM 或者请求数据这在原生自定义元素里是不行的。标准规定constructor阶段不能读写元素属性、不能添加子元素因为此时元素还没有被插入文档不安全的操作会导致异常。正确姿势是全部放到connectedCallback里做。2.2 Shadow DOM样式隔离的真正核心Shadow DOM 是 Web Components 里最有价值的设计。它让组件拥有一棵独立的 DOM 树这棵树上的节点对外部是不可见的外部的 CSS 选择器选不到它内部的元素内部的 CSS 规则也不会泄漏出去。这就是“样式隔离”。用生活里的事打比方你买了一台电视机外壳可以打开但里面的电路板是被罩住的。电视机的造型你可以随意更换但内部电路不会因为你在外壳上贴了个贴纸就短路也不会影响你家其他电器。Shadow DOM 就是这个罩子。它的原理是怎样的呢浏览器在渲染时会为主文档构造一棵 DOM 树而每个 Shadow Root 相当于在宿主元素下面挂了一棵独立的树。两棵树在渲染时会被合并成一棵渲染树但样式计算是分开作用域的。外部样式作用不到内部因为 CSS 匹配是从 Shadow Root 内部开始向上找的内部样式也不会影响外部因为它们作用域限定在这棵 Shadow 树内。要注意的是Shadow DOM 的隔离不是绝对的。你可以通过 CSS 变量自定义属性穿透进去比如:host可以设置宿主元素自身的样式:host-context()可以根据外部上下文调整样式::part()可以允许外部样式化组件内部特定部分。这些是 Shadow DOM 留给外部世界的“缝隙”是刻意设计出来的可控接口而不是漏洞。2.3 Template 与 Slot积木的接口与插槽template标签很有意思浏览器解析它时会把内部内容解析成 DOM但不渲染、不加载图片、不执行脚本。只有在需要的时候你通过template.content.cloneNode(true)把它实例化出来插入到 DOM 里。这就相当于积木的“模具”你想生成多少个组件实例只需要从模具里复制即可。Slot插槽则定义了组件如何接受外部内容。它就像是插座组件内部定义好slot的位置外部传入的内容会被分发到对应位置。比如一个dialog组件内部预留一个slot作为标题区一个slot作为内容区调用者只需要往里面填东西就行。通过slot你实现的是“组件内容组合”而不是每次复制粘贴。2.4 为什么说这就像搭积木积木有几个特性每块有标准接口、每块内部实心实料、块与块之间可以自由组合、可以任意拆开重新拼。Web Components 正好对上了这几个特征。标准接口对应 Custom Elements 标签本身通过属性和事件进行交互内部实心对应 Shadow DOM 的封装的逻辑和样式自由组合对应slot和组合式 API 的能力任意拆卸对应组件生命周期里前后端分离的挂载/卸载。这样一来单个按钮是积木表单是积木一个完整的商品卡片是积木甚至整个页面也能抽象成一个大型积木组件化终于摆脱了框架的地基。3. 实操从零构建一个可复用的商品卡片组件3.1 原材料准备不需要构建工具开始动手之前先说一下环境准备。Web Components 是浏览器原生能力你不需要安装 Node.js、不需要 webpack、不需要 Babel。新建一个.html文件直接写代码用浏览器打开就能看到效果。Chrome、Edge、Firefox、Safari 等主流浏览器最近几年都已原生支持四大 API兼容性已经不需要担心。如果要兼容老版本浏览器那才需要引入 polyfill但今天这个演示我们不考虑古早环境。建议在本地起一个静态服务器来打开页面因为 ES Modules 是通过http或https协议加载的直接用file://打开本地文件容易遇到 CORS 限制。如果你电脑上有 Node.js可以用npx serve一键起一个静态服务器没有 Node 的话用 VS Code 的 Live Server 插件也一样。3.2 最小可运行的 Hello 组件我们先从最原始的、没有用 Shadow DOM 的设计开始方便理解自定义元素的生命周期逻辑。!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleWeb Components 演示/title /head body hello-world name晨风/hello-world script class HelloWorld extends HTMLElement { constructor() { super(); this._name 世界; } static get observedAttributes() { return [name]; } connectedCallback() { this.render(); } attributeChangedCallback(name, oldValue, newValue) { if (name name) { this._name newValue; this.render(); } } render() { this.innerHTML p你好${this._name}/p; } } customElements.define(hello-world, HelloWorld); /script /body /html把这文件在浏览器里打开你会看到一个“你好晨风”的段落。这个组件做了几件简单的事定义了一个新标签hello-world声明它关注name属性属性变化时重新渲染内容。注意render方法是在connectedCallback里访问和修改 DOM 的这符合规范。不过这种写法有个致命问题组件的样式和内部结构完全暴露在全局作用域里如果页面上有一段 CSS 恰好选中了p就会影响组件内部的外观。要做到真正的封装就要引入 Shadow DOM。3.3 升级使用 Shadow DOM 提升封装性在刚才的基础上做三个改动。第一在构造函数里调用this.attachShadow({ mode: open })创建 Shadow Root第二把所有内部节点挂到this.shadowRoot下第三把样式写进 Shadow Root 内部。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title商品卡片组件演示/title style /* 外部全局样式 */ p { color: red; font-size: 40px; } /style /head body product-card name机械键盘 price499 description茶轴、RGB、87键布局办公游戏两相宜 imagehttps://example.com/keyboard.png /product-card script class ProductCard extends HTMLElement { constructor() { super(); this.attachShadow({ mode: open }); } static get observedAttributes() { return [name, price, description, image]; } connectedCallback() { this.render(); } attributeChangedCallback() { this.render(); } render() { const name this.getAttribute(name) || 默认商品; const price this.getAttribute(price) || 0; const desc this.getAttribute(description) || ; const image this.getAttribute(image) || ; this.shadowRoot.innerHTML style .card { border: 1px solid #e0e0e0; border-radius: 12px; padding: 16px; width: 260px; display: flex; flex-direction: column; gap: 12px; font-family: system-ui, sans-serif; background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.06); } .card img { width: 100%; height: 140px; object-fit: cover; border-radius: 8px; background: #f5f5f5; } .name { font-size: 18px; font-weight: 600; margin: 0; } .price { color: #f5222d; font-size: 20px; font-weight: 700; } .desc { color: #666; font-size: 14px; line-height: 1.5; margin: 0; } .buy-btn { background: #f5222d; color: #fff; border: none; border-radius: 6px; padding: 10px; font-size: 14px; cursor: pointer; } /style div classcard ${image ? img src${image} alt${name} : } h3 classname${name}/h3 div classprice¥ ${price}/div p classdesc${desc}/p button classbuy-btn加购/button /div ; const btn this.shadowRoot.querySelector(.buy-btn); btn.addEventListener(click, () { this.dispatchEvent(new CustomEvent(add-to-cart, { detail: { name, price: Number(price) }, bubbles: true, composed: true })); }); } } customElements.define(product-card, ProductCard); /script /body /html现在这个组件有几个明显进步页面上那个“把所有p变成红色大字”的全局样式失效了吗你可以试试确实没作用到组件内部的.desc段落。因为浏览器在匹配样式时到达 Shadow Root 边界就无法继续穿透了这就是 Shadow DOM 的隔离特性。还有一个细节值得说明我在触发事件时加了bubbles: true, composed: true。bubbles表示事件会冒泡composed表示事件可以穿透 Shadow DOM 边界。如果不设置composed: true外部监听器就收不到这个事件这是 Shadow DOM 事件隔离的机制。很多新手在这里踩坑后面我会专门讲。3.4 进一步封装修饰继承默认行为很多场景下我们希望给一个已有的原生元素增强功能。比如给img加懒加载或者给button加埋点。使用 Custom Elements 时你想继承原生 HTML 元素的能力只需要继承对应的类并通过extends属性指定它。class LazyImage extends HTMLImageElement { connectedCallback() { const src this.getAttribute(src); if (src) { this.loading lazy; this.addEventListener(load, () { this.dispatchEvent(new CustomEvent(image-loaded, { bubbles: true })); }); } } } customElements.define(lazy-image, LazyImage, { extends: img });这种语法有时会引发混淆注意customElements.define的第三个参数{ extends: img }。使用这个组件时不能写lazy-image而要写img islazy-image这是标准规定的“自定义内置元素”用法。不过主流开发者对is的接受度不算高如果你只是想给图片加个 loading 属性完全可以直接写成独立的lazy-image底层内部创建img外部接口保持一致即可。工程上我更倾向于后者因为可读性更好也不依赖特殊行为。4. 浏览器底层原理组件背后发生了什么4.1 HTML Parser 如何解释自定义标签浏览器加载页面时会经过一个非常成熟的解析流程HTML Parser 扫描字节流把 HTML 字符串解析成 DOM 树。遇到未知标签时浏览器并不会报错而是默默地创建一个 HTMLElement。回到我们的例子里HTML Parser 在文档中看到product-card会创建一个 HTMLElement放在 DOM 树中。此时这个元素本身还没有任何特定行为直到 JavaScript 执行了customElements.define浏览器会立即在内部做一次“升级”。它会检查文档中现有的所有元素找到所有名为product-card的元素然后用自定义元素类重新构造它们的实例。这个升级时机有个常见的坑如果你的脚本在页面底部执行那没问题因为元素已经在文档里了。但是如果页面还有未下载完的异步脚本、动态插入的组件标签那升级动作就得等后续define执行时统一处理。自定义元素类定义之后后续创建的元素会立即使用这个类不需要等待。4.2 Shadow DOM 的渲染树构建与样式计算渲染核心工作流里有一步叫“样式计算”Style Calculation。浏览器根据 CSS 规则和 DOM 结构计算出每个节点的最终样式。没有 Shadow DOM 时CSS 匹配是全局的从 HTML 文档根部开始向下匹配所有元素。有了 Shadow DOM 后情况发生了变化。每个 Shadow Root 是一个独立的样式作用域。当浏览器遇到一个宿主元素发现它包含 Shadow Root 时会把 Shadow 树挂在渲染树上但样式的匹配过程会先在 Shadow Root 内部做一遍然后再回到主文档树的父链上。换句话说即使在全局样式表中写一个* { color: red !important; }也作用不到 Shadow DOM 内部的段落。这是因为浏览器的样式匹配在进入 Shadow 树后匹配上下文被重置了。这个隔离机制不是靠什么权重控制实现的而是建立在渲染引擎的数据结构层面的所以非常可靠。不过要注意继承属性比如font-family除外继承是从宿主元素向下传给 Shadow 树内部的这也解释了为什么很多组件工程上会给 Shadow 内部的根节点显式设置字体和颜色。4.3 组件生命周期与浏览器事件循环自定义元素的回调是由浏览器内部的任务队列触发的具体时机和微任务、宏任务的调度有关。connectedCallback是在元素被插入可访问的 DOM 后触发的表现形式上是同步的实际发生在 DOM 操作导致重绘之前的微任务阶段。如果你在同一个 tick 里连续把元素移出又插回文档浏览器可能会合并操作只触发一次connectedCallback。这些细节在实际项目里影响不大但你要建立一种意识生命周期回调并不意味着“每执行一次 DOM 操作就必然触发一次”它是与浏览器渲染调度紧密相关的。4.4 与框架协作在 React / Vue 中使用 Web Components很多人关心项目是 React 或者 Vue 的能不能用 Web Components答案是能而且相当顺手。在 React 里使用自定义元素把它当作一个普通标签即可。但是要注意两点React 会把未知属性当作字符串设置到 DOM 上所以你给组件传非字符串类型时会发生类型问题自定义元素的原生事件要绑定在 ref 上通过addEventListener注册可以避免 React 的合成事件兼容差异。function App() { const productRef useRef(null); useEffect(() { const node productRef.current; const handler (e) { console.log(收到加购事件, e.detail); }; node.addEventListener(add-to-cart, handler); return () node.removeEventListener(add-to-cart, handler); }, []); return ( product-card ref{productRef} name{机械键盘} price{499} description{茶轴、RGB、87键布局} / ); }在 Vue 里就更自然了Vue 的模板引擎支持未知标签并且会默认当作原生元素处理属性和事件绑定方式基本通用。唯一要注意的是Vue 3 里给自定义元素传对象类型属性时需要用到.prop修饰符如:config.propconfigObject这才能正确调用setAttribute之外的属性赋值路径。5. 常见问题与排查技巧实录5.1 样式不生效别忘检查:host和::part使用 Shadow DOM 时最容易遇到的问题就是“全局样式怎么传不进去”这是预期的隔离行为不是 bug。如果你确实需要外部控制组件样式优先用 CSS 变量。组件内部定义:host { --primary-color: #1677ff; --font-size-base: 14px; }外部使用product-card { --primary-color: #f5222d; --font-size-base: 16px; }:host伪类代表的是宿主元素本身。外部样式直接设置product-card的display、margin是可以生效的因为这些在宿主上但想优雅地设置组件内部特定元素就要用::part()。在组件模板块里给元素加上partthumbnail之类的标识然后外部通过product-card::part(thumbnail)定向设置既保留了封装性又提供了可控的接口。5.2 事件收不到composed是关键我刚才提过Shadow DOM 内部触发的事件默认不会冒泡到外部文档。如果外部的监听器收不到事件第一反应就检查事件对象创建时有没有加composed: true。btn.addEventListener(click, (e) { this.dispatchEvent(new CustomEvent(add-to-cart, { detail: { ... }, bubbles: true, composed: true })); });有了composed: true事件跨越 Shadow Root 边界就像穿透了一层透明的膜外部可以正常监听到。这个技巧我几乎在每个组件里都会用到建议你直接形成肌肉记忆。5.3 属性变化不触发更新observedAttributes必不可少自定义元素不是魔术浏览器不可能监听所有属性。它只会监听你在类的静态字段observedAttributes里列出的属性。如果你的attributeChangedCallback一直没触发先看看observedAttributes里有没有对应属性名。另一个常见问题是在attributeChangedCallback里调用render()但是组件还没连接到文档导致this.shadowRoot是空的。原因是attributeChangedCallback在connectedCallback之前也可能被触发。稳妥的做法是在attributeChangedCallback里加个判断attributeChangedCallback(name, oldValue, newValue) { if (this.shadowRoot) { this.render(); } }5.4 表单数据的获取原生实现比想象中容易自定义元素如果不做特殊处理它在表单里跟普通div一样数据不会自动提交到后端。所幸规范提供了 Form Associated Custom Element表单关联自定义元素只要实现ElementInternals的setFormValue方法就可以参与表单提交。class MyDatePicker extends HTMLElement { constructor() { super(); this._internals this.attachInternals(); } set value(val) { this._value val; this._internals.setFormValue(val); } get value() { return this._value; } }如果你不想深入表单关联机制也可以在组件里放一个隐藏的input元素模拟旧式做法这样即便不上attachInternals也能把值带到表单里。但既然原生方案已经有头有尾建议项目允许时直接用标准能力。6. 工程化落地组件库的组织与性能优化6.1 不用框架组件库怎么组织没有框架的削足适履不代表可以随意写代码。工程化的第一步是用标准 ES Modules 拆文件。把每个组件放到单独文件内部自己管理样式和脚本让每个文件都是可独立发布的模块。推荐的目录结构src/ components/ product-card/ index.js product-card.js product-card.css elements/ index.js每个组件的index.js负责customElements.define()而外层elements/index.js负责汇总导入所有组件。对外发布时保留这个结构用户只需要按需import对应文件即可。6.2 依赖管理和版本化Web Components 之间也可以存在依赖关系。比如product-card内部用到了price-tag组件那product-card模块应该显式import ./price-tag.js。组件库发布到 npm 上用户通过包管理器安装依赖关系自然清晰。在实际项目中我建议每个组件库模块都声明自己依赖的组件名避免“隐式依赖”造成幽灵 bug。6.3 性能优化批量注册、模板复用与渲染时机批量注册自定义元素其实是浏览器规范推荐的customElements.define本身有热度缓存但批量定义还可以减少主线程解析脚本的时间。可以把所有组件模块合并成一个 bundle 一次加载也可以只加载首屏组件后面懒加载注册其他组件。懒加载的方式很简单动态import()那个组件的模块模块内执行define即可。模板复用方面template能带来很好的性能收益。你要是准备好了把组件结构写进template里实例化时用content.cloneNode(true)比每次innerHTML拼接字符串再解析的效率高得多。我早期写组件图省事直接innerHTML模板字符串到了高频渲染场景实测比template方式慢了将近一倍。后来全部改为template方式效果立竿见影。还有一个很容易被忽略的优化点避免在attributeChangedCallback里做重计算。回调频率高每次属性变化都会触发你要谨慎地在里面放复杂查询或者大 DOM 操作。更合理的做法是把它当作“标记”把真实更新推迟到requestAnimationFrame或setTimeout里批量执行。6.4 实际项目里应该什么时候用 Web Components这是我在团队里被问得最多的问题。我的建议是分三类场景第一跨项目复用基础组件。如果你在一家公司同时维护多个项目甚至技术栈不同把“按钮、弹窗、日期选择、表单控件”做成 Web Components收益最明显所有项目都能用还不用重复维护几套。第二对框架锁定风险敏感的中长期项目。如果甲方或者公司说五年十年后可能换技术栈那用 Web Components 搭基础层将来框架切换时组件层基本不受影响。第三纯原生/轻量前端项目。数据交互简单、页面逻辑不复杂的站点比如宣传页、活动页可以直接用 Web Components 杜绝引入重型框架交互体验也不差。不适合的场景重度依赖状态管理、复杂路由嵌套、需要强响应式数据流的应用。这种情况下强行用 Web Components 会导致状态流转代码分散维护起来反而吃力。这类项目老老实实用框架让 Web Components 做框架里的“积木零件”就好了。7. 可访问性与国际化容易被忽略的组件品质组件不但要能跑还要让更多人用得舒服这就是可访问性的意义。自定义元素本身不会自动提供任何可访问性语义如果你组件内部用了大量div屏幕阅读器可能只把它当成一个普通的文本块。解决思路也很标准要么利用原生标签的语义比如按钮用button标题用h1到h6要么通过 ARIA 属性补充在组件里定义一个 role 属性例如rolealert表示更新的与铁人应用区域aria-label给不直观的控件一个可读名字。Shadow DOM 的内部结构对外部可访问性树来说是黑盒所以对每个起到实际交互作用的元素你都要显式标注出语义属性。国际化主要是“文案”和“格式”两层问题。文案上尽量在组件里用getAttribute(i18n-key)这样的接口交给上层读取字典渲染格式上诸如price、date这类属性使用统一标准格式传入比如价格传数字和货币代码日期传 ISO 字符串而不是本地化字符串这样组件内部可以挑选正确的区域进行格式化避免做一层“假国际化”的欺软怕硬式适配。8. 一些最终想说的经验之谈做了几年组件封装我最大的感受是Web Components 的核心价值不是替代框架而是给“组合”这件事提供了最低成本的通用语言。它把人从“框架兼容”这件内耗中解放出来让你可以专心打磨组件本身的交互和逻辑。如果你打算从零开始我建议不要一上来就挑战大而全的组件库先从一个小工具组件起步比如一个带规范校验的日期输入框或者一个支持懒加载的图片框把它做成一个标准 Web Components然后在真实项目里试用一遍。跑通一次“定义—使用—优化—发布”的流程比看十篇文章都管用。还有一个体会生态虽然越来越成熟但 Web Components 目前更像是一把瑞士军刀而不是一台自动化生产线。项目里聪明地组合原生能力与框架优势才能获得最大的开发效率和长期可维护性。我自己现在写项目的习惯已经变成了“基础组件全用 Web Components业务逻辑交给框架”这套思路经过几个项目的验证沉淀组件的时间成本并没有变高IDC 之后跨项目复用却省了相当多的重复开发。希望这篇关于浏览器原生组件机制与应用的拆解能帮你找到自己项目里“搭积木”的感觉。