HTML不提供分页逻辑,customElements.define()是唯一能真正封装自定义分页组件行为的原生方案;class或div.pagination仅是样式命名,非组件,无生命周期、属性响应、样式隔离等能力。

HTML 本身不提供分页逻辑,customElements.define() 是唯一能真正封装“自定义分页组件”行为的原生方案;用 class 或 div.pagination 只是样式命名,不是组件。
为什么不能直接用 class="pagination" 当作自定义组件
很多人把带 class 的 <div class="pagination"> 误认为是“自定义组件”,其实它只是普通 DOM 节点:没有生命周期、无法接收属性、不能响应 <code>page 变化、也不隔离样式。浏览器根本不识别它为组件,JS 里查不到 instanceof HTMLElement,也无法通过 document.querySelector('pagination-component') 精准定位。
- 所有交互逻辑(如点击页码)必须手动绑定到全局选择器,容易冲突
- 样式靠 CSS 类名污染全局作用域,改一个项目里的 .pagination 可能崩掉另一个模块
- 无法通过
<pagination-component total="100" current="3"></pagination-component>声明式初始化
怎么用 customElements.define() 实现可复用分页组件
核心是继承 HTMLElement,监听属性变更,并在 connectedCallback 中渲染 UI。以下是最简可行骨架:
class PaginationComponent extends HTMLElement {
static get observedAttributes() {
return ['total', 'current', 'size'];
}
<p>get total() { return +this.getAttribute('total') || 0; }
get current() { return +this.getAttribute('current') || 1; }
get size() { return +this.getAttribute('size') || 10; }</p><p>connectedCallback() {
this.render();
}</p><p>attributeChangedCallback() {
this.render();
}</p><p>render() {
const totalPages = Math.ceil(this.total / this.size);
const html = `</p><nav aria-label="分页导航"><button type="button" :>‹ 上一页</button>
${Array.from({ length: totalPages }, (_, i) => i + 1)
.map(i => `<button type="button" this.current class="active" :>${i}</button>`)
.join('')}
<button type="button">= totalPages ? 'disabled' : ''}>下一页 ›</button></nav>
`;
this.innerHTML = html;
<pre class="brush:php;toolbar:false;">// 绑定事件(注意:每次 render 都要重绑,或用事件委托)
this.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', e => {
if (e.target.textContent.includes('上一页')) {
this.setAttribute('current', Math.max(1, this.current - 1));
} else if (e.target.textContent.includes('下一页')) {
this.setAttribute('current', Math.min(totalPages, this.current + 1));
} else if (/^\d+$/.test(e.target.textContent)) {
this.setAttribute('current', e.target.textContent);
}
});
});} }
customElements.define('pagination-component', PaginationComponent);
- 必须用
button而非a,避免默认跳转和 SEO 干扰(当前页用aria-current="page"标识) - 属性名全小写,不要用
currentPage这类驼峰——HTML 属性不区分大小写,current-page才是规范写法 -
render()里直接赋值this.innerHTML简单粗暴,但若需支持 slot 插入内容,得改用Shadow DOM+this.attachShadow({ mode: 'open' })
用 Shadow DOM 隔离样式和事件时要注意什么
加了 Shadow DOM 后,外部 CSS 无法穿透,组件内按钮点击也不会冒泡到 document,这是优点也是坑:
- 必须在 Shadow Root 内显式注入 CSS,例如
const style = document.createElement('style'); style.textContent = '.pagination button {...}'; shadow.appendChild(style); - 父级无法监听
pagination-component内部按钮的 click,得靠自定义事件:this.dispatchEvent(new CustomEvent('page-change', { detail: { page: newPage } })); - 如果组件需要响应 URL 参数(如 ?page=5),不能只靠属性监听——得在
connectedCallback里读new URLSearchParams(location.search).get('page')并同步到current属性
为什么多数项目不该从零手写 Web Component 分页
真正在生产环境用 customElements.define() 做分页,会立刻撞上三个现实约束:
- IE11 和旧版 Safari 不支持,哪怕加了 polyfill,
Shadow DOM的兼容性也极差 - 服务端渲染(SSR)时,组件标签会被当作文本原样输出,
connectedCallback不执行,首屏无分页按钮 - 无法直接和 React/Vue 的响应式系统联动——你不能把
current属性绑定到 Vue 的ref,得靠事件桥接
真正该用 Web Component 的场景,是构建设计系统中高度复用、强隔离需求的原子组件(比如内部管理后台的独立分页控件),而不是业务页面里一个要和 API、路由、状态管理深度耦合的分页器。











