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