webComponent:像搭积木一样构建web应用|浏览器篇
# webComponent:像搭积木一样构建web应用
30 秒速记
- 组件化的判断标准是内部职责和状态集中,外部依赖少,并通过明确、简单的接口协作。
- 组件独立后,多人可以分别开发和测试,内部状态不应越过组件边界干扰其他模块。
- 函数作用域、块级作用域和对象封装已经让
JavaScript具备隐藏状态、暴露接口的能力。 - 前端组件不只有逻辑,还包含
HTML结构和CSS样式;仅封装JavaScript无法完成整个视图的隔离。 Web Components要解决的是局部视图的原生封装问题,重点在于同时管理元素结构、样式和行为的边界。
组件化的核心是对内高内聚、对外低耦合,让组件通过简单、明确的接口协作。 这样多人开发时,每个人都能独立实现和测试自己的模块,内部状态也不会越过边界影响其他组件。JavaScript 依靠函数作用域、块级作用域和对象封装,已经能隐藏状态并暴露接口。不过前端组件还包含 HTML 和 CSS,所以 Web Components 关注的是把结构、样式和行为一起封装起来。
什么是组件化呢?
其实组件化并没有一个明确的定义,不过这里我们可以使用 10 个字来形容什么是组件化,那就是:对内高内聚,对外低耦合。对内各个元素彼此紧密结合、相互依赖,对外和其他组件的联系最少且接口简单。
可以说,程序员对组件化开发有着天生的需求,因为一个稍微复杂点的项目,就涉及到多人协作开发的问题,每个人负责的组件需要尽可能独立完成自己的功能,其组件的内部状态不能影响到别人的组件,在需要和其他组件交互的地方得提前协商好接口。通过组件化可以降低整个系统的耦合度,同时也降低程序员之间沟通复杂度,让系统变得更加易于维护。
使用组件化能带来很多优势,所以很多语言天生就对组件化提供了很好的支持,比如 C/C++ 就可以很好地将功能封装成模块,无论是业务逻辑,还是基础功能,抑或是 UI,都能很好地将其组合在一起,实现组件内部的高度内聚、组件之间的低耦合。
大部分语言都能实现组件化,归根结底在于编程语言特性,大多数语言都有自己的函数级作用域、块级作用域和类,可以将内部的状态数据隐藏在作用域之下或者对象的内部,这样外部就无法访问了,然后通过约定好的接口和外部进行通信。
JavaScript 虽然有不少缺点,但是作为一门编程语言,它也能很好地实现组件化,毕竟有自己的函数级作用域和块级作用域,所以封装内部状态数据并提供接口给外部都是没有问题的。
既然 JavaScript 可以很好地实现组件化,那么我们所谈论的 WebComponent 到底又是什么呢?
面试官追问
追问 1代码评审里有人把用户卡片的几个函数放进一个对象,就宣称这个页面已经完成组件化,你认同吗?
不能仅凭函数归组认定完整组件化,这最多封装了部分 JavaScript 状态和接口。页面组件还涉及结构与样式边界,外部若能任意干预内部状态或产生跨模块影响,仍未达到对内高内聚、对外低耦合。
追问 2一个由五名开发共同维护的后台页面已经拆成十几个文件,但改动筛选区仍会导致表格模块行为异常,你会用什么标准检查拆分质量?
应检查各模块内部状态是否真正隐藏,以及跨模块通信是否只经过预先约定的简单接口。文件拆分只是物理组织,若模块仍共享可变状态或任意访问彼此内部实现,耦合和协作成本并没有实质下降。
追问 3业务要把第三方订单视图嵌入现有页面,负责人只要求其数据逻辑独立,却允许样式和节点结构相互影响,这算满足组件化目标吗?
只能算完成了部分逻辑封装,尚未覆盖完整视图的组件边界。组件内部状态不应随意影响其他组件,跨组件联系也应尽量少且接口明确;若第三方结构或样式可能外溢,就需要继续约束这些影响。
追问 4线上出现“修改导航模块后详情模块状态被重置”的故障,两边代码位于不同目录且由不同团队负责,你会优先排查什么?
应优先追踪两个模块是否共享状态、全局变量或绕过约定接口直接调用内部逻辑。目录和团队边界不能自动形成组件边界;若交互协议不清晰,即使代码物理隔离,内部状态仍可能跨模块传播。
追问 5架构师主张只用普通 JavaScript 的函数作用域和类完成大型页面组件化,另一方坚持所有模块都必须采用 WebComponent,你会怎样取舍?
若目标只是隐藏逻辑状态并通过明确接口通信,JavaScript 的函数作用域、块级作用域和类已经具备封装能力。若组件还要求完整视图的结构与样式边界,则仅封装脚本可能不足;是否采用 WebComponent 应由所需隔离范围决定,而不是统一套用。
# 阻碍前端组件化的因素
30 秒速记
- 前端组件化的主要障碍来自共享的
CSS匹配环境和可被全局操作的页面DOM。 - 不同模块都声明
p选择器时,样式会进入同一套CSSOM匹配过程,集成后可能覆盖或混合彼此的视觉效果。 - 页面只有一套全局
DOM入口,任意脚本都可能读取或修改其他模块的节点,结构边界无法仅靠文件拆分保证。 - 模块单独测试正常不代表集成安全;命名碰撞和跨模块节点操作通常在合并页面后才暴露。
JavaScript的作用域能够保护逻辑状态,但不能自动限制全局选择器和全局DOM API的影响范围。
阻碍前端组件化的关键是 CSS 默认全局匹配,而页面中的 DOM 又能被任意脚本读取和修改。 两个模块分别开发时都写 p 选择器,单独测试可能正常,但合并后会进入同一套 CSSOM 匹配过程,样式就可能互相影响。类似地,文件拆开并不等于节点隔离,脚本仍可能跨模块操作页面元素。JavaScript 作用域可以保护逻辑状态,却不会自动限制选择器和全局 DOM API 的影响范围。
在前端虽然 HTML、CSS 和 JavaScript 是强大的开发语言,但是在大型项目中维护起来会比较困难,如果在页面中嵌入第三方内容时,还需要确保第三方的内容样式不会影响到当前内容,同样也要确保当前的 DOM 不会影响到第三方的内容。
所以要聊 WebComponent,得先看看 HTML 和 CSS 是如何阻碍前端组件化的,这里我们就通过下面这样一个简单的例子来分析下:
<style>
p {
background-color: brown;
color: cornsilk
}
</style>
<p>time.geekbang.org</p>
<style>
p {
background-color: red;
color: blue
}
<p>time.geekbang</p>
上面这两段代码分别实现了自己 p 标签的属性,如果两个人分别负责开发这两段代码的话,那么在测试阶段可能没有什么问题,不过当最终项目整合的时候,其中内部的 CSS 属性会影响到其他外部的 p 标签的,之所以会这样,是因为 CSS 是影响全局的。
我们在《23 | 渲染流水线:CSS 如何影响首次加载时的白屏时间?》这篇文章中分析过,渲染引擎会将所有的 CSS 内容解析为 CSSOM,在生成布局树的时候,会在 CSSOM 中为布局树中的元素查找样式,所以有两个相同标签最终所显示出来的效果是一样的,渲染引擎是不能为它们分别单独设置样式的。
除了 CSS 的全局属性会阻碍组件化,DOM 也是阻碍组件化的一个因素,因为在页面中只有一个 DOM,任何地方都可以直接读取和修改 DOM。所以使用 JavaScript 来实现组件化是没有问题的,但是 JavaScript 一旦遇上 CSS 和 DOM,那么就相当难办了
面试官追问
追问 1两个团队各自交付的模块单独测试都正常,合并到首页后两边的 p 标签却同时变成红底蓝字,为什么不能把责任简单归为某个模块加载失败?
这更像全局 CSS 规则冲突,而不是模块加载失败。页面中的样式会被统一解析进 CSSOM,布局树生成时,同类元素会在同一作用域内匹配规则,并不会按团队或文件自动隔离;最终效果还可能受层叠关系影响。
追问 2代码评审中,组件负责人把模板、样式和逻辑分别放进三个独立文件,便声称首页上的五个组件已经互不影响,你会用什么运行时现象反驳?
拆分文件只改善代码组织,不能建立运行时边界。样式载入页面后仍可能匹配其他组件的节点,脚本也能通过全局 DOM API 读取或修改组件外元素;若要隔离,必须限制 CSS 与 DOM 的实际作用范围。
追问 3第三方活动页必须直接嵌入主站,但双方都大量使用 p、div 等标签选择器,而且不能统一改类名,你会把接入验收重点放在哪里?
验收重点应放在双向样式污染和脚本越界修改上。仅验证第三方页面自身显示正常不够,还要检查主站规则是否改变其节点、其规则是否进入全局 CSSOM,以及脚本能否触达主站节点;靠命名约定补救会带来持续协作成本。
追问 4线上只有营销模块发布后,文章区的段落颜色被改写,但网络面板和节点结构都正常,你会怎样判断是 CSSOM 冲突还是 DOM 被脚本改动?
先检查文章节点的最终匹配规则和计算样式,确认是否有营销模块的全局选择器命中;再观察节点属性、类名或结构是否被脚本动态修改。两类故障都源于缺少边界,但修复点不同,不能只删除一条表面上可疑的样式规则。
追问 5架构会上有人主张继续靠选择器前缀治理组件污染,另一方要求引入具备局部作用域的封装机制,你会如何判断取舍?
前缀约定适合协作范围可控且能持续审查的代码,但无法从机制上阻止全局规则或脚本越界。第三方内容、多团队并行或长期演进时,更应选择能约束 CSS 和 DOM 作用域的方案;代价是接入方式、调试路径和公开接口都需要重新设计。
# WebComponent 组件化开发
30 秒速记
Web Components由Custom Elements、Shadow DOM和HTML Templates共同完成局部视图封装。<template>保存可复用的结构与样式,但模板内容本身不会直接进入布局并显示,需要克隆后挂载。- 自定义元素类继承
HTMLElement,在构造过程中读取模板、调用attachShadow()创建影子根,再把模板副本加入其中。 customElements.define()将类注册为自定义标签,注册后可像普通HTML元素一样多次使用。Shadow DOM将内部节点和样式限制在局部边界内;全局节点查询不能直接穿透该边界,访问内部内容要从约定入口进行。- 隔离重点是
DOM和CSS,不是为JavaScript另建一套语言作用域;脚本仍需依赖JavaScript自身的封装能力。
Web Components 通过自定义元素、Shadow DOM 和 <template>,把局部视图的结构、样式与行为封装起来。 <template> 用来保存可复用内容,但不会直接参与页面布局,需要克隆后挂载到影子根。组件类继承 HTMLElement,通过 attachShadow() 创建边界,再用 customElements.define() 注册成可重复使用的标签。这里主要隔离的是 DOM 和 CSS,JavaScript 仍要依靠自身的作用域和封装机制管理状态。
