原生Web Components提供了自定义元素、shadow DOM、HTML模板等核心能力,能够让开发者在不依赖任何前端框架的情况下,实现高内聚低耦合的组件化开发,进而构建出可维护的复杂应用。下面我们将逐步讲解完整的构建流程。

核心基础概念
要构建基于Web Components的复杂应用,首先需要理解三个核心特性:
- 自定义元素:允许开发者定义自己的HTML标签,浏览器会将其作为普通HTML元素解析,支持自定义属性和方法。
- shadow DOM:为组件创建独立的DOM和样式作用域,避免组件内部的样式和逻辑与外部产生冲突。
- HTML模板:通过<template>和<slot>标签定义组件的可复用结构和内容分发规则。
定义基础自定义元素
首先我们通过继承HTMLElement类来定义第一个基础组件,这里以一个按钮组件为例:
// 定义自定义元素类
class MyButton extends HTMLElement {
// 指定需要监听的属性变化
static get observedAttributes() {
return ['text', 'type'];
}
// 元素挂载到DOM时触发
connectedCallback() {
this.render();
}
// 监听的属性发生变化时触发
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
// 渲染组件内容
render() {
const text = this.getAttribute('text') || '默认按钮';
const type = this.getAttribute('type') || 'default';
// 设置组件内部内容
this.innerHTML = `
<style>
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.default {
background: #f0f0f0;
color: #333;
}
.primary {
background: #1890ff;
color: #fff;
}
</style>
<button class="btn ${type}">${text}</button>
`;
}
}
// 注册自定义元素,标签名为my-button
customElements.define('my-button', MyButton);
注册完成后,我们就可以在HTML中直接使用这个组件:
<my-button text="点击提交" type="primary"></my-button> <my-button text="取消操作" type="default"></my-button>
使用shadow DOM实现样式隔离
上面的基础组件没有使用shadow DOM,内部样式可能会影响到外部元素,也不受外部样式影响,实际复杂应用中建议使用shadow DOM实现真正的隔离:
class ShadowButton extends HTMLElement {
static get observedAttributes() {
return ['text', 'type'];
}
constructor() {
super();
// 创建shadow DOM,mode为open表示外部可以通过元素.shadowRoot访问
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
render() {
const text = this.getAttribute('text') || '默认按钮';
const type = this.getAttribute('type') || 'default';
// 向shadow DOM中插入内容
this.shadowRoot.innerHTML = `
<style>
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.default {
background: #f0f0f0;
color: #333;
}
.primary {
background: #1890ff;
color: #fff;
}
/* 这里的样式只会作用于当前组件的shadow DOM内部 */
</style>
<button class="btn ${type}">${text}</button>
`;
}
}
customElements.define('shadow-button', ShadowButton);
复杂组件的结构设计
构建复杂应用需要拆分多个不同层级的组件,比如我们可以拆分出布局组件、列表组件、表单组件等。以一个简单的用户列表组件为例,使用<template>和<slot>实现内容分发:
<!-- 定义列表项的模板 -->
<template id="user-item-template">
<style>
.user-item {
display: flex;
align-items: center;
padding: 12px;
border-bottom: 1px solid #eee;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 12px;
}
.info {
flex: 1;
}
.name {
font-weight: bold;
margin-bottom: 4px;
}
.desc {
color: #666;
font-size: 12px;
}
</style>
<div class="user-item">
<img class="avatar" src="" />
<div class="info">
<div class="name"><slot name="name">默认名称</slot></div>
<div class="desc"><slot name="desc">默认描述</slot></div>
</div>
</div>
</template>
对应的自定义元素逻辑:
class UserItem extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// 获取模板内容
const template = document.getElementById('user-item-template');
const content = template.content.cloneNode(true);
// 设置头像地址
const avatar = content.querySelector('.avatar');
avatar.src = this.getAttribute('avatar') || 'https://picsum.photos/40/40?random=1';
// 将模板内容插入shadow DOM
this.shadowRoot.appendChild(content);
}
}
customElements.define('user-item', UserItem);
使用时可以通过slot插入自定义内容:
<user-item avatar="https://picsum.photos/40/40?random=2"> <span slot="name">张三</span> <span slot="desc">前端开发工程师</span> </user-item>
跨组件状态管理
复杂应用必然涉及跨组件的状态共享,我们可以使用发布订阅模式实现一个简单的全局状态管理:
// 全局状态管理器
class Store {
constructor() {
this.state = {};
this.listeners = new Map();
}
// 更新状态并通知所有监听者
setState(key, value) {
this.state[key] = value;
if (this.listeners.has(key)) {
this.listeners.get(key).forEach(callback => callback(value));
}
}
// 获取状态
getState(key) {
return this.state[key];
}
// 监听状态变化
subscribe(key, callback) {
if (!this.listeners.has(key)) {
this.listeners.set(key, []);
}
this.listeners.get(key).push(callback);
}
}
// 创建全局单例store
const globalStore = new Store();
在组件中监听状态变化:
class StatusDisplay extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `<div>当前状态:<span id="status">未加载</span></div>`;
}
connectedCallback() {
// 监听userStatus状态变化
globalStore.subscribe('userStatus', (status) => {
this.shadowRoot.querySelector('#status').textContent = status;
});
}
}
customElements.define('status-display', StatusDisplay);
// 另一个组件更新状态
class StatusUpdater extends HTMLElement {
connectedCallback() {
this.innerHTML = '<button>更新状态</button>';
this.querySelector('button').addEventListener('click', () => {
globalStore.setState('userStatus', '已登录');
});
}
}
customElements.define('status-updater', StatusUpdater);
应用路由实现
复杂应用通常需要路由功能,我们可以基于原生History API实现简单的路由:
class Router {
constructor() {
this.routes = new Map();
this.currentComponent = null;
// 监听浏览器前进后退
window.addEventListener('popstate', () => {
this.render(location.pathname);
});
}
// 注册路由
register(path, componentName) {
this.routes.set(path, componentName);
}
// 跳转路由
navigate(path) {
history.pushState(null, '', path);
this.render(path);
}
// 渲染对应路由的组件
render(path) {
const appContainer = document.getElementById('app');
// 移除当前组件
if (this.currentComponent) {
this.currentComponent.remove();
}
const componentName = this.routes.get(path) || this.routes.get('/');
if (componentName) {
const component = document.createElement(componentName);
appContainer.appendChild(component);
this.currentComponent = component;
}
}
}
const router = new Router();
// 注册路由
router.register('/', 'home-page');
router.register('/user', 'user-page');
// 初始渲染
router.render(location.pathname);
应用整合与优化建议
当所有组件开发完成后,我们可以按照功能模块整合整个应用,同时需要注意以下优化点:
- 组件的属性变化尽量做节流处理,避免频繁触发渲染。
- 对于不常变化的静态内容,可以使用缓存减少重复渲染开销。
- 复杂组件可以拆分更小的子组件,保持单个组件的职责单一。
- 可以使用自定义事件实现组件间的直接通信,减少全局状态的滥用。
通过以上步骤,我们就可以完全基于原生Web Components构建一个功能完整的复杂应用,整个过程不需要引入任何第三方框架,应用的包体积会更小,同时也能享受到浏览器原生能力的支持,兼容性也能覆盖大部分现代浏览器。
Web_Components自定义元素shadow_DOM无框架开发修改时间:2026-07-01 16:57:59