创建 SVG 图标预览页面:从零实现到解决 CORS 问题
问题描述
在项目开发中,我们有一个包含大量 SVG 图标的 icon.js 文件,这些图标以 SVG <symbol> 的形式存储。为了方便开发团队查看和使用这些图标,我们需要创建一个静态 HTML 页面来预览所有图标。
需求分析
- 展示所有图标:以网格形式展示 icon.js 中的所有 SVG 图标
- 图标预览:每个图标以合适的大小显示,方便查看
- 点击复制:点击图标可以复制使用代码,提高开发效率
- 统计信息:显示图标总数
- 美观的界面:提供良好的用户体验
实现过程
第一步:分析 icon.js 文件结构
首先,我们需要了解 icon.js 文件的工作原理。查看文件内容发现:
javascript
!function(e){
var t,n,d,o,i,a,r='<svg><symbol id="success" viewBox="0 0 48 48" fill="none">...</symbol>...';
t=function(){
var e,t,n;
(n=document.createElement("div")).innerHTML=r,
r=null,
(t=n.getElementsByTagName("svg")[0])&&(
t.setAttribute("aria-hidden","true"),
t.style.position="absolute",
t.style.width=0,
t.style.height=0,
t.style.overflow="hidden",
e=t,
(n=document.body).firstChild?
(t=n.firstChild).parentNode.insertBefore(e,t):
n.appendChild(e)
)
};
document.addEventListener?
["complete","loaded","interactive"].indexOf(document.readyState)>-1?
setTimeout(t,0):
(n=function(){
document.removeEventListener("DOMContentLoaded",n,!1),
t()
},
document.addEventListener("DOMContentLoaded",n,!1)):
document.attachEvent&&/* IE8 及以下处理 */;
}(window);
关键发现:
- icon.js 是一个立即执行函数
- 它将 SVG 内容存储在变量
r中 - 通过创建一个临时 div,将 SVG 插入到 DOM 中
- SVG 被设置为隐藏状态(position: absolute, width: 0, height: 0)
- 在页面加载完成后自动执行
第二步:创建基础 HTML 结构
创建 public/icon-preview.html 文件,包含基本的 HTML 结构和样式:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Icon Preview</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 40px;
}
.header h1 {
color: #333;
font-size: 32px;
margin-bottom: 10px;
}
.header p {
color: #666;
font-size: 14px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 20px;
}
.icon-card {
background: white;
border-radius: 8px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
}
.icon-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
.icon-preview {
width: 64px;
height: 64px;
margin: 0 auto 12px;
display: flex;
align-items: center;
justify-content: center;
}
.icon-preview svg {
width: 100%;
height: 100%;
}
.icon-name {
font-size: 13px;
color: #666;
word-break: break-all;
}
.copy-hint {
position: fixed;
bottom: 20px;
right: 20px;
background: #333;
color: white;
padding: 12px 24px;
border-radius: 4px;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.copy-hint.show {
opacity: 1;
}
.stats {
text-align: center;
margin-bottom: 30px;
color: #666;
font-size: 14px;
}
</style>
</head>
<body>
<div class="header">
<h1>Icon Preview</h1>
<p>点击图标复制使用代码</p>
</div>
<div class="container">
<div class="stats" id="stats"></div>
<div class="icon-grid" id="iconGrid"></div>
</div>
<div class="copy-hint" id="copyHint">已复制到剪贴板</div>
</body>
</html>
第三步:实现图标提取和渲染逻辑
添加 JavaScript 代码来提取和渲染图标:
javascript
<script src="icon.js"></script>
<script>
const iconGrid = document.getElementById('iconGrid');
const copyHint = document.getElementById('copyHint');
const stats = document.getElementById('stats');
function extractIconIds() {
const ids = [];
const symbols = document.querySelectorAll('symbol');
symbols.forEach(symbol => {
const id = symbol.getAttribute('id');
if (id) {
ids.push(id);
}
});
return ids;
}
function createIconCard(iconId) {
const card = document.createElement('div');
card.className = 'icon-card';
const preview = document.createElement('div');
preview.className = 'icon-preview';
preview.innerHTML = `<svg><use href="#${iconId}"></use></svg>`;
const name = document.createElement('div');
name.className = 'icon-name';
name.textContent = iconId;
card.appendChild(preview);
card.appendChild(name);
card.addEventListener('click', () => {
const code = `<svg><use href="#${iconId}"></use></svg>`;
navigator.clipboard.writeText(code).then(() => {
showCopyHint();
});
});
return card;
}
function showCopyHint() {
copyHint.classList.add('show');
setTimeout(() => {
copyHint.classList.remove('show');
}, 2000);
}
function init() {
const iconIds = extractIconIds();
stats.textContent = `共 ${iconIds.length} 个图标`;
iconIds.forEach(iconId => {
const card = createIconCard(iconId);
iconGrid.appendChild(card);
});
}
init();
</script>
第四步:遇到 CORS 问题
直接在浏览器中打开 HTML 文件时,遇到了以下错误:
sql
Access to XMLHttpRequest at 'file:///E:/all_projects/HuLa/public/icon.js'
from origin 'null' has been blocked by CORS policy:
Cross origin requests are only supported for protocol schemes: http, data,
isolated-app, chrome-extension, chrome, https, chrome-untrusted.
问题原因:
- 最初尝试使用
XMLHttpRequest读取 icon.js 文件内容 - 直接打开 HTML 文件(file:// 协议)时,浏览器出于安全考虑禁止跨域请求
- 即使是同目录下的文件,也会被浏览器视为跨域
解决方案 : 既然 icon.js 会自动将 SVG 插入到 DOM 中,我们不需要通过 AJAX 读取文件,而是直接从 DOM 中提取 <symbol> 元素。
第五步:优化初始化时机
由于 icon.js 是异步加载的,我们需要确保在 icon.js 执行完成后再初始化图标列表:
javascript
function init() {
const iconIds = extractIconIds();
stats.textContent = `共 ${iconIds.length} 个图标`;
iconIds.forEach(iconId => {
const card = createIconCard(iconId);
iconGrid.appendChild(card);
});
}
if (document.readyState === 'complete') {
setTimeout(init, 100);
} else {
window.addEventListener('load', () => {
setTimeout(init, 100);
});
}
关键改进:
- 检查
document.readyState判断页面是否已加载完成 - 如果已完成,延迟 100ms 执行初始化
- 如果未完成,监听
load事件后再执行 - 100ms 的延迟确保 icon.js 有足够时间将 SVG 插入 DOM
最终代码
完整的 icon-preview.html 文件:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Icon Preview</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 40px;
}
.header h1 {
color: #333;
font-size: 32px;
margin-bottom: 10px;
}
.header p {
color: #666;
font-size: 14px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 20px;
}
.icon-card {
background: white;
border-radius: 8px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
}
.icon-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
.icon-preview {
width: 64px;
height: 64px;
margin: 0 auto 12px;
display: flex;
align-items: center;
justify-content: center;
}
.icon-preview svg {
width: 100%;
height: 100%;
}
.icon-name {
font-size: 13px;
color: #666;
word-break: break-all;
}
.copy-hint {
position: fixed;
bottom: 20px;
right: 20px;
background: #333;
color: white;
padding: 12px 24px;
border-radius: 4px;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.copy-hint.show {
opacity: 1;
}
.stats {
text-align: center;
margin-bottom: 30px;
color: #666;
font-size: 14px;
}
</style>
</head>
<body>
<div class="header">
<h1>Icon Preview</h1>
<p>点击图标复制使用代码</p>
</div>
<div class="container">
<div class="stats" id="stats"></div>
<div class="icon-grid" id="iconGrid"></div>
</div>
<div class="copy-hint" id="copyHint">已复制到剪贴板</div>
<script src="icon.js"></script>
<script>
const iconGrid = document.getElementById('iconGrid');
const copyHint = document.getElementById('copyHint');
const stats = document.getElementById('stats');
function extractIconIds() {
const ids = [];
const symbols = document.querySelectorAll('symbol');
symbols.forEach(symbol => {
const id = symbol.getAttribute('id');
if (id) {
ids.push(id);
}
});
return ids;
}
function createIconCard(iconId) {
const card = document.createElement('div');
card.className = 'icon-card';
const preview = document.createElement('div');
preview.className = 'icon-preview';
preview.innerHTML = `<svg><use href="#${iconId}"></use></svg>`;
const name = document.createElement('div');
name.className = 'icon-name';
name.textContent = iconId;
card.appendChild(preview);
card.appendChild(name);
card.addEventListener('click', () => {
const code = `<svg><use href="#${iconId}"></use></svg>`;
navigator.clipboard.writeText(code).then(() => {
showCopyHint();
});
});
return card;
}
function showCopyHint() {
copyHint.classList.add('show');
setTimeout(() => {
copyHint.classList.remove('show');
}, 2000);
}
function init() {
const iconIds = extractIconIds();
stats.textContent = `共 ${iconIds.length} 个图标`;
iconIds.forEach(iconId => {
const card = createIconCard(iconId);
iconGrid.appendChild(card);
});
}
if (document.readyState === 'complete') {
setTimeout(init, 100);
} else {
window.addEventListener('load', () => {
setTimeout(init, 100);
});
}
</script>
</body>
</html>
关键知识点
SVG Symbol 和 Use 元素
SVG Symbol:
<symbol>元素用于定义可重用的图形模板- 类似于组件,可以在页面中多次引用
- 通常放在一个隐藏的 SVG 容器中
Use 元素:
<use>元素用于引用已定义的 symbol- 通过
href或xlink:href属性指定 symbol 的 id - 示例:
<svg><use href="#icon-name"></use></svg>
优势:
- 减少代码重复
- 便于统一管理和修改
- 提高性能(symbol 只需解析一次)
icon.js 工作原理
icon.js 是一个自包含的 SVG 图标库,具有以下特点:
- 立即执行函数:页面加载时自动执行
- DOM 注入:将 SVG 插入到页面中
- 隐藏显示:SVG 元素被设置为不可见
- 按需引用 :通过
<use>元素引用具体的图标
CORS 跨域问题
什么是 CORS:
- Cross-Origin Resource Sharing(跨域资源共享)
- 浏览器的安全机制,限制从一个源加载的文档或脚本如何与另一个源的资源进行交互
file:// 协议的限制:
- 直接打开 HTML 文件时,origin 为
null - 浏览器禁止从
nullorigin 发起 AJAX 请求 - 即使是同目录下的文件也会被限制
解决方案:
- 使用本地服务器(如 http-server、live-server)
- 避免使用 AJAX 读取文件
- 利用已加载的资源(如本例中的 icon.js)
DOM 操作最佳实践
查询元素:
document.querySelectorAll():查询所有匹配的元素document.querySelector():查询第一个匹配的元素
创建元素:
document.createElement():创建新元素element.appendChild():添加子元素element.innerHTML:设置 HTML 内容
事件处理:
addEventListener():添加事件监听器setTimeout():延迟执行window.load事件:确保资源加载完成
使用方法
- 直接打开 :在浏览器中打开
public/icon-preview.html文件 - 查看图标:页面会自动加载并显示所有图标
- 复制代码:点击任意图标,自动复制使用代码到剪贴板
- 使用图标:在项目中使用复制的代码引用图标
扩展功能建议
如果需要进一步增强功能,可以考虑:
- 搜索功能:添加搜索框,快速查找特定图标
- 分类显示:按图标类型或用途分类展示
- 颜色自定义:提供颜色选择器,实时预览不同颜色的图标
- 尺寸调整:允许调整图标预览大小
- 导出功能:将选中的图标导出为独立 SVG 文件
- 代码生成:生成不同框架的使用代码(Vue、React 等)
总结
通过创建 icon-preview.html,我们成功实现了一个简洁实用的 SVG 图标预览工具。在实现过程中,我们:
- 分析了 icon.js 的工作原理,理解了 SVG symbol 的使用方式
- 遇到了 CORS 跨域问题,并通过避免 AJAX 请求解决了这个问题
- 优化了初始化时机,确保在 icon.js 加载完成后再渲染图标
- 提供了良好的用户体验,包括网格布局、悬停效果、点击复制等功能
这个工具不仅方便了开发团队查看和使用图标,也展示了如何在不依赖复杂框架的情况下,使用原生 HTML/CSS/JavaScript 创建实用的工具。