创建 SVG 图标预览页面:从零实现到解决 CORS 问题

创建 SVG 图标预览页面:从零实现到解决 CORS 问题

问题描述

在项目开发中,我们有一个包含大量 SVG 图标的 icon.js 文件,这些图标以 SVG <symbol> 的形式存储。为了方便开发团队查看和使用这些图标,我们需要创建一个静态 HTML 页面来预览所有图标。

需求分析

  1. 展示所有图标:以网格形式展示 icon.js 中的所有 SVG 图标
  2. 图标预览:每个图标以合适的大小显示,方便查看
  3. 点击复制:点击图标可以复制使用代码,提高开发效率
  4. 统计信息:显示图标总数
  5. 美观的界面:提供良好的用户体验

实现过程

第一步:分析 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);
    });
}

关键改进:

  1. 检查 document.readyState 判断页面是否已加载完成
  2. 如果已完成,延迟 100ms 执行初始化
  3. 如果未完成,监听 load 事件后再执行
  4. 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 图标库,具有以下特点:

  1. 立即执行函数:页面加载时自动执行
  2. DOM 注入:将 SVG 插入到页面中
  3. 隐藏显示:SVG 元素被设置为不可见
  4. 按需引用 :通过 <use> 元素引用具体的图标

CORS 跨域问题

什么是 CORS:

  • Cross-Origin Resource Sharing(跨域资源共享)
  • 浏览器的安全机制,限制从一个源加载的文档或脚本如何与另一个源的资源进行交互

file:// 协议的限制:

  • 直接打开 HTML 文件时,origin 为 null
  • 浏览器禁止从 null origin 发起 AJAX 请求
  • 即使是同目录下的文件也会被限制

解决方案:

  1. 使用本地服务器(如 http-server、live-server)
  2. 避免使用 AJAX 读取文件
  3. 利用已加载的资源(如本例中的 icon.js)

DOM 操作最佳实践

查询元素:

  • document.querySelectorAll():查询所有匹配的元素
  • document.querySelector():查询第一个匹配的元素

创建元素:

  • document.createElement():创建新元素
  • element.appendChild():添加子元素
  • element.innerHTML:设置 HTML 内容

事件处理:

  • addEventListener():添加事件监听器
  • setTimeout():延迟执行
  • window.load 事件:确保资源加载完成

使用方法

  1. 直接打开 :在浏览器中打开 public/icon-preview.html 文件
  2. 查看图标:页面会自动加载并显示所有图标
  3. 复制代码:点击任意图标,自动复制使用代码到剪贴板
  4. 使用图标:在项目中使用复制的代码引用图标

扩展功能建议

如果需要进一步增强功能,可以考虑:

  1. 搜索功能:添加搜索框,快速查找特定图标
  2. 分类显示:按图标类型或用途分类展示
  3. 颜色自定义:提供颜色选择器,实时预览不同颜色的图标
  4. 尺寸调整:允许调整图标预览大小
  5. 导出功能:将选中的图标导出为独立 SVG 文件
  6. 代码生成:生成不同框架的使用代码(Vue、React 等)

总结

通过创建 icon-preview.html,我们成功实现了一个简洁实用的 SVG 图标预览工具。在实现过程中,我们:

  1. 分析了 icon.js 的工作原理,理解了 SVG symbol 的使用方式
  2. 遇到了 CORS 跨域问题,并通过避免 AJAX 请求解决了这个问题
  3. 优化了初始化时机,确保在 icon.js 加载完成后再渲染图标
  4. 提供了良好的用户体验,包括网格布局、悬停效果、点击复制等功能

这个工具不仅方便了开发团队查看和使用图标,也展示了如何在不依赖复杂框架的情况下,使用原生 HTML/CSS/JavaScript 创建实用的工具。

参考资料

相关推荐
静默回滚1 小时前
iPhone照片电脑上打不开:HEIC解码从原理到WASM
前端
dsyyyyy11012 小时前
Vue 3 Watch 监视完全指南
前端·javascript·vue.js
航飞光电市场经理2 小时前
人员定位系统“全栈自研”技术解析:从射频前端到定位引擎的架构拆解
前端·架构
Apifox2 小时前
Apifox 9 月更新|CLI 能力升级、GitLab 私有化部署接入与产品体验优化
前端·后端·测试
deli0072 小时前
随机乱跳为什么能画出完美三角形:混沌游戏分形实验室
前端
Gizzap_Tech2 小时前
国内官网增加英文版:URL、语言切换与 hreflang 怎么配置?
前端
剪刀石头布啊2 小时前
泛洪DFS、BFS
前端
qetfw2 小时前
Windows Server AD CS:根 CA、Web 证书模板与域内自动注册
前端·windows·windows-server
剪刀石头布啊2 小时前
为什么React组件很少前缀,而vue的不少组件都有前缀
前端