HTML模板元素提供了一种声明式的方式来定义可复用的HTML片段。本章将介绍template元素和slot元素的使用方法。
<template id="card-template">
<style>
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
margin: 16px 0;
}
.card-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
}
.card-content {
color: #666;
}
</style>
<div class="card">
<div class="card-title"></div>
<div class="card-content"></div>
</div>
</template>
<script>
function createCard(title, content) {
const template = document.getElementById('card-template');
const clone = template.content.cloneNode(true);
clone.querySelector('.card-title').textContent = title;
clone.querySelector('.card-content').textContent = content;
return clone;
}
document.body.appendChild(createCard('标题', '内容'));
</script>
const templateFeatures = {
inert: '模板内容在克隆前不会被渲染',
hidden: '模板内容默认隐藏',
script: '模板内的脚本不会执行',
style: '模板内的样式不会应用',
images: '模板内的图片不会加载',
content: '通过template.content访问DocumentFragment'
};
class TemplateManager {
constructor() {
this.templates = new Map();
}
register(name, template) {
if (typeof template === 'string') {
const element = document.createElement('template');
element.innerHTML = template;
this.templates.set(name, element);
} else {
this.templates.set(name, template);
}
}
get(name) {
return this.templates.get(name);
}
instantiate(name, data = {}) {
const template = this.templates.get(name);
if (!template) return null;
const clone = template.content.cloneNode(true);
this.bindData(clone, data);
return clone;
}
bindData(fragment, data) {
const elements = fragment.querySelectorAll('[data-bind]');
elements.forEach(element => {
const key = element.getAttribute('data-bind');
const value = this.getNestedValue(data, key);
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
element.value = value;
} else {
element.textContent = value;
}
});
const attrElements = fragment.querySelectorAll('[data-bind-attr]');
attrElements.forEach(element => {
const config = JSON.parse(element.getAttribute('data-bind-attr'));
Object.entries(config).forEach(([attr, key]) => {
element.setAttribute(attr, this.getNestedValue(data, key));
});
});
}
getNestedValue(obj, path) {
return path.split('.').reduce((acc, part) => acc?.[part], obj);
}
}
const templateManager = new TemplateManager();
templateManager.register('user-card', `
<div class="user-card">
<img data-bind-attr='{"src": "avatar", "alt": "name"}'>
<h3 data-bind="name"></h3>
<p data-bind="email"></p>
</div>
`);
const userCard = templateManager.instantiate('user-card', {
name: '张三',
email: 'zhangsan@example.com',
avatar: '/avatar.jpg'
});
<template id="panel-template">
<style>
.panel {
border: 1px solid #ccc;
border-radius: 8px;
overflow: hidden;
}
.panel-header {
background: #f5f5f5;
padding: 12px;
border-bottom: 1px solid #ccc;
}
.panel-body {
padding: 16px;
}
.panel-footer {
background: #f5f5f5;
padding: 12px;
border-top: 1px solid #ccc;
}
</style>
<div class="panel">
<div class="panel-header">
<slot name="header">默认标题</slot>
</div>
<div class="panel-body">
<slot>默认内容</slot>
</div>
<div class="panel-footer">
<slot name="footer">默认底部</slot>
</div>
</div>
</template>
class SlotComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
const template = document.getElementById('panel-template');
this.shadowRoot.appendChild(template.content.cloneNode(true));
this.shadowRoot.querySelectorAll('slot').forEach(slot => {
slot.addEventListener('slotchange', (e) => {
console.log(`插槽 ${slot.name || 'default'} 内容变化`);
console.log('分配的节点:', slot.assignedNodes());
console.log('分配的元素:', slot.assignedElements());
});
});
}
getSlotContent(slotName) {
const slot = this.shadowRoot.querySelector(
slotName ? `slot[name="${slotName}"]` : 'slot:not([name])'
);
return slot ? slot.assignedNodes() : [];
}
getSlotElements(slotName) {
const slot = this.shadowRoot.querySelector(
slotName ? `slot[name="${slotName}"]` : 'slot:not([name])'
);
return slot ? slot.assignedElements() : [];
}
}
class ConditionalSlot extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
render() {
const hasHeader = this.querySelector('[slot="header"]');
const hasFooter = this.querySelector('[slot="footer"]');
this.shadowRoot.innerHTML = `
<style>
.container { border: 1px solid #ccc; }
.header, .footer { background: #f5f5f5; padding: 8px; }
.content { padding: 16px; }
</style>
<div class="container">
${hasHeader ? '<div class="header"><slot name="header"></slot></div>' : ''}
<div class="content"><slot></slot></div>
${hasFooter ? '<div class="footer"><slot name="footer"></slot></div>' : ''}
</div>
`;
}
}
class TemplateInheritance {
constructor() {
this.baseTemplates = new Map();
this.derivedTemplates = new Map();
}
defineBase(name, template) {
this.baseTemplates.set(name, template);
}
define(name, baseName, extensions) {
const baseTemplate = this.baseTemplates.get(baseName);
if (!baseTemplate) throw new Error(`基础模板 ${baseName} 不存在`);
const derived = document.createElement('template');
derived.innerHTML = baseTemplate.innerHTML;
Object.entries(extensions).forEach(([selector, content]) => {
const element = derived.content.querySelector(selector);
if (element) {
if (typeof content === 'string') {
element.innerHTML = content;
} else if (content.append) {
element.appendChild(content.cloneNode(true));
}
}
});
this.derivedTemplates.set(name, derived);
}
get(name) {
return this.derivedTemplates.get(name) || this.baseTemplates.get(name);
}
}
const inheritance = new TemplateInheritance();
inheritance.defineBase('base-card', `
<div class="card">
<div class="header"><slot name="header"></slot></div>
<div class="body"><slot></slot></div>
<div class="footer"><slot name="footer"></slot></div>
</div>
`);
inheritance.define('image-card', 'base-card', {
'.header': '<slot name="header"></slot><slot name="image"></slot>'
});
class TemplateCompiler {
constructor() {
this.compiled = new Map();
}
compile(template) {
const html = typeof template === 'string'
? template
: template.innerHTML;
const tokens = this.tokenize(html);
const render = this.generateRender(tokens);
return render;
}
tokenize(html) {
const tokens = [];
const regex = /\{\{([^}]+)\}\}|\{%\s*(\w+)\s+([^%]+)\s*%\}|\{%\s*end(\w+)\s*%\}/g;
let lastIndex = 0;
let match;
while ((match = regex.exec(html)) !== null) {
if (match.index > lastIndex) {
tokens.push({ type: 'text', value: html.slice(lastIndex, match.index) });
}
if (match[1]) {
tokens.push({ type: 'variable', value: match[1].trim() });
} else if (match[2]) {
tokens.push({ type: 'block', name: match[2], value: match[3].trim() });
} else if (match[4]) {
tokens.push({ type: 'endblock', name: match[4] });
}
lastIndex = regex.lastIndex;
}
if (lastIndex < html.length) {
tokens.push({ type: 'text', value: html.slice(lastIndex) });
}
return tokens;
}
generateRender(tokens) {
return (data) => {
let result = '';
let i = 0;
while (i < tokens.length) {
const token = tokens[i];
switch (token.type) {
case 'text':
result += token.value;
break;
case 'variable':
result += this.getValue(data, token.value);
break;
case 'block':
if (token.name === 'for') {
const [item, list] = token.value.split(' in ').map(s => s.trim());
const endToken = this.findEndBlock(tokens, i, 'for');
const blockTokens = tokens.slice(i + 1, endToken);
const items = this.getValue(data, list) || [];
items.forEach(itemData => {
result += this.generateRender(blockTokens)({ ...data, [item]: itemData });
});
i = endToken;
} else if (token.name === 'if') {
const condition = this.getValue(data, token.value);
const endToken = this.findEndBlock(tokens, i, 'if');
const blockTokens = tokens.slice(i + 1, endToken);
if (condition) {
result += this.generateRender(blockTokens)(data);
}
i = endToken;
}
break;
}
i++;
}
return result;
};
}
findEndBlock(tokens, start, name) {
let depth = 1;
for (let i = start + 1; i < tokens.length; i++) {
if (tokens[i].type === 'block' && tokens[i].name === name) {
depth++;
} else if (tokens[i].type === 'endblock' && tokens[i].name === name) {
depth--;
if (depth === 0) return i;
}
}
return tokens.length;
}
getValue(data, path) {
return path.split('.').reduce((obj, key) => obj?.[key], data);
}
}
const compiler = new TemplateCompiler();
const render = compiler.compile(`
<ul>
{% for item in items %}
<li>{{item.name}}: {{item.price}}</li>
{% endfor %}
</ul>
`);
const html = render({
items: [
{ name: '商品A', price: 100 },
{ name: '商品B', price: 200 }
]
});
📋 模板使用建议
- 复用性:将通用结构定义为模板
- 性能:模板只解析一次,多次克隆使用
- 维护性:集中管理模板,便于修改
- 类型安全:结合TypeScript提供数据类型检查
🔄 模板与组件
- template适合静态结构
- Web Components适合交互组件
- 可以结合使用:组件内部使用模板
下一章将探讨 模块化开发,学习前端模块化开发的最佳实践。