关于Data URLs svg图片显示出错和浏览器URL hash #

本文探讨了使用Data URLs格式的SVG图片在某些浏览器中出现显示异常的问题,特别是当URL包含特殊字符并作为hash时。73版谷歌浏览器和66版火狐浏览器对特定Data URLs的解析出现错误,可能是因为浏览器内部将其识别为位置标识符,导致数据丢失和解析异常。建议在使用Data URLs时注意URL编码和转义,以避免此类问题。

在使用生成的svg图作为<img>标签是src值时,发现有部分浏览器显示异常,所以这里记录下

参考链接

Data URLs
http://www.faqs.org/rfcs/rfc2397.html
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/data_URIs
URL hash
http://www.ruanyifeng.com/blog/2011/03/url_hash.html
https://developer.mozilla.org/zh-CN/docs/Web/API/URL/hash

<img src="Data URLs">中,Data URLs格式与显示情况如下:

//1. 部分浏览器不能正常显示
data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50"><rect fill="#795548" x="0" y="0" width="100%" height="100%"></rect><text fill="#FFF" x="50%" y="50%" text-anchor="middle" alignment-baseline="central" font-size="16" font-family="Verdana, Geneva, sans-serif">jack</text></svg>

//2. 采用base64编码svg,正常显示
data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MCIgaGVpZ2h0PSI1MCI+PHJlY3QgZmlsbD0iIzc5NTU0OCIgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSI+PC9yZWN0Pjx0ZXh0IGZpbGw9IiNGRkYiIHg9IjUwJSIgeT0iNTAlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBhbGlnbm1lbnQtYmFzZWxpbmU9ImNlbnRyYWwiIGZvbnQtc2l6ZT0iMTYiIGZvbnQtZmFtaWx5PSJWZXJkYW5hLCBHZW5ldmEsIHNhbnMtc2VyaWYiPmphY2s8L3RleHQ+PC9zdmc+

//3. 采用%23转义#,正常显示
data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50"><rect fill="%23795548" x="0" y="0" width="100%" height="100%"></rect><text fill="%23FFF" x="50%" y="50%" text-anchor="middle" alignment-baseline="central" font-size="16" font-family="Verdana, Geneva, sans-serif">jack</text></svg>

//4. 采用rgb代替hex color,正常显示
data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50"><rect fill="rgb(121,85,72)" x="0" y="0" width="100%" height="100%"></rect><text fill="rgb(255,255,255)" x="50%" y="50%" text-anchor="middle" alignment-baseline="central" font-size="16" font-family="Verdana, Geneva, sans-serif">jack</text></svg>

上面给出的Data URLs中第一个与其他的不同之处就是包含了URL的敏感字符#,其被作为hash使用,用于浏览器网页内部的网页位置指定标识符,#后面出现的任何字符,都会被浏览器解读为位置标识符。

这里我用以上链接直接使用浏览器访问,73版谷歌浏览器和66版火狐浏览器对于第一个Data URLs给出的结果都是解析异常,这里我的猜测(意淫)就是这种Data URLs其实是浏览器内部识别了URL标识,其又充当了一台服务器,对当前Data URLs进行解析,之后内部直接给出数据。而它们在处理data:image/svg+xml时将#后面的字符串当做为位置标识符,没有将#后数据提交至浏览器内部解析器(我认为的模拟服务器)中,所以就出现了数据丢失解析异常。

以上分析纯属个人猜测。反正这里需要注意的就是,采用Data URLs时有可能出现URL特殊字符,最好能够对其进行编码,或者转义。

(function (global, factory) { typeof exports === &#39;object&#39; && typeof module !== &#39;undefined&#39; ? module.exports = factory() : typeof define === &#39;function&#39; && define.amd ? define(factory) : (global.$fetch = factory()); }(typeof self !== &#39;undefined&#39; ? self : window, function () { &#39;use strict&#39;; class fetchFactory { constructor() { this.defaultOptions = { timeout: 10000, retries: 3, retryDelay: 1000 }; } /** * 创建AbortController */ createController() { return new AbortController(); } /** * 检测文件类型 */ detectFileType(contentType, url) { if (!contentType && url) { const ext = url.split(&#39;.&#39;).pop()?.toLowerCase(); if ([&#39;jpg&#39;, &#39;jpeg&#39;, &#39;png&#39;, &#39;gif&#39;, &#39;bmp&#39;, &#39;webp&#39;, &#39;svg&#39;].includes(ext)) { return &#39;image&#39;; } if ([&#39;json&#39;].includes(ext)) { return &#39;json&#39;; } if ([&#39;txt&#39;, &#39;md&#39;, &#39;js&#39;, &#39;css&#39;, &#39;html&#39;, &#39;xml&#39; , &#39;vue&#39;, &#39;ts&#39;, &#39;jsx&#39;, &#39;scss&#39;].includes(ext)) { return &#39;text&#39;; } } if (contentType) { if (contentType.startsWith(&#39;image/&#39;)) return &#39;image&#39;; if (contentType.includes(&#39;json&#39;)) return &#39;json&#39;; if (contentType.startsWith(&#39;text/&#39;)) return &#39;text&#39;; } return &#39;binary&#39;; } /** * 图片专用转换 (FileReader) * @param {Blob} blob * @returns {Promise<string>} Base64字符串 */ toBase64(blob) { let isImage = false; if (!blob.type) return this.txtToBase64(blob) if (blob.type.startsWith(&#39;image/&#39;)) { console.log(`检测到图片格式: ${blob.type}`); isImage = true; } else { console.log(`其他文件格式: ${blob.type || &#39;未知&#39;}`); return this.txtToBase64(blob) } return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); reader.onerror = (err) => reject(isImage ? &#39;图片转换失败:&#39; + err : err); reader.readAsDataURL(blob); }); } txtToBase64(str = &#39;&#39;) { if (!str || typeof str !== &#39;string&#39;) return str // 创建 TextEncoder 实例 const encoder = new TextEncoder(); // 将字符串编码为 Uint8Array const data = encoder.encode(str); // 将字节数组转换为二进制字符串 const binary = Array.from(data, byte => String.fromCharCode(byte)).join(&#39;&#39;); // 编码为 Base64 return btoa(binary); } /** * 自动处理响应数据 */ async handleResponse(response, options = {}) { const contentType = response.headers.get(&#39;content-type&#39;) || &#39;&#39;; const url = response.url; const fileType = this.detectFileType(contentType, url); let data; try { switch (options.readMode || &#39;auto&#39;) { case &#39;blob&#39;: data = await response.blob(); break; case &#39;text&#39;: data = await response.text(); break; case &#39;json&#39;: data = await response.json(); break; case &#39;arrayBuffer&#39;: data = await response.arrayBuffer(); break; case &#39;auto&#39;: default: if (fileType === &#39;text&#39;) { data = await response.text(); } else if (fileType === &#39;json&#39;) { data = await response.json(); } else { data = await response.blob(); } } } catch (error) { // 如果解析失败,返回原始文本 data = await response.text(); } return { data, fileType, contentType, size: response.headers.get(&#39;content-length&#39;) || 0 }; } /** * 核心请求方法 */ async request(url, options = {}) { const opts = { ...this.defaultOptions, ...options }; const controller = opts.signal || this.createController(); let result = { url, code: null, msg: &#39;&#39;, data: null, loaded: 0, total: 0, duration: 0 }; const startTime = Date.now(); let timeoutId = null; try { // 请求前回调 if (opts.before) { await opts.before(url, opts); } if (opts.platform) { result.platform = opts.platform; delete opts.platform; } // 设置超时处理 if (opts.timeout > 0) { timeoutId = setTimeout(() => { controller.abort(); }, opts.timeout); } const response = await fetch(url, { method: &#39;GET&#39;, ...opts, signal: controller.signal }); result.code = response.status; // 处理特殊状态码 switch (response.status) { case 200: case 201: case 204: result.msg = &#39;ok&#39;; break; case 429: result.msg = &#39;请求频率超限,请稍后重试&#39;; break; case 401: result.msg = &#39;未授权,请检查认证信息&#39;; break; case 403: result.msg = &#39;禁止访问&#39;; break; case 404: result.msg = &#39;资源未找到&#39;; break; case 500: result.msg = &#39;服务器内部错误&#39;; break; default: result.msg = `请求失败: ${response.statusText}`; } // 处理响应数据 const handledData = await this.handleResponse(response, opts); result.data = handledData.data; result.fileType = handledData.fileType; result.contentType = handledData.contentType; result.size = handledData.size; // 流式进度处理 if (opts.progress && response.body && handledData.fileType !== &#39;text&#39; && handledData.fileType !== &#39;json&#39;) { const reader = response.body.getReader(); const contentLength = response.headers.get(&#39;content-length&#39;); result.total = parseInt(contentLength) || 0; const chunks = []; let loaded = 0; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); loaded += value.length; result.loaded = loaded; opts.progress(url, loaded, result.total); } // 重新组合数据 if (opts.readMode === &#39;blob&#39; || (opts.readMode === &#39;auto&#39; && handledData.fileType === &#39;binary&#39;)) { result.data = new Blob(chunks, { type: handledData.contentType }); } } } catch (error) { if (error.name === &#39;AbortError&#39;) { result.code = 0; result.msg = &#39;请求已取消&#39;; } else { result.code = error.code || -1; result.msg = error.message || &#39;网络错误&#39;; } } finally { // 在finally块中清除超时定时器,确保无论如何都会清理 if (timeoutId) { clearTimeout(timeoutId); } result.duration = Date.now() - startTime; // 完成后回调 if (opts.after) { await opts.after(url, result); } } return result; } /** * 带代理支持的请求方法 */ async requestWithProxy(url, options = {}) { const { proxies, ...opts } = options; // 如果没有提供代理,使用普通请求 if (!proxies || !Array.isArray(proxies) || proxies.length === 0) { return await this.request(url, opts); } // 尝试使用代理 let lastError = null; for (const proxy of proxies) { try { // 设置3秒超时 const proxyOptions = { timeout: 5000, ...opts, }; // 构建代理URL let proxyUrl; if (typeof proxy === &#39;string&#39;) { proxyUrl = `${proxy}${url}`; } else { if (proxy.perfix) { // 直接将完整URL拼接到代理URL后面 proxyUrl = `${proxy.url}${url}`; } else { // 只将路径部分拼接到代理URL后面 const urlObj = new URL(url); const path = urlObj.pathname + urlObj.search + urlObj.hash; proxyUrl = `${proxy.url}${path}`; } } const result = await this.request(proxyUrl, proxyOptions); // 如果请求成功,返回结果 if (result.code >= 200 && result.code < 300) { return result; } // 如果请求失败,记录错误并尝试下一个代理 lastError = result; } catch (error) { lastError = { code: -1, msg: error.message, url: proxy.url }; } } // 所有代理都失败,返回最后一个错误 return lastError || { code: -1, msg: &#39;所有代理请求失败&#39;, url }; } /** * 并发下载 */ async downloadConcurrent(urls, options = {}) { const maxConcurrency = options.maxConcurrency || 3; const controller = new ConcurrencyController(maxConcurrency); const tasks = urls.map(url => () => this.requestWithProxy(url, options) ); const promises = tasks.map(task => controller.add(task)); return await Promise.allSettled(promises); } /** * 并行上传 */ async uploadParallel(file, platforms, options = {}) { const uploadPromises = platforms.map(async (platform) => { try { // 判断上传类型 if (file && typeof file === &#39;object&#39;) { // 根据平台调整请求体格式 const requestBody = { content: file.content, message: file.message, ...(platform.branch && { branch: platform.branch }) }; options.body = JSON.stringify(requestBody); // 确保Content-Type为application/json options.headers = { &#39;Content-Type&#39;: &#39;application/json&#39;, ...platform.headers }; } else { // 文件上传模式 - 使用原始文件对象 options.body = file; } options.method = options.method || &#39;POST&#39;; const result = await this.request(platform.url, options); return { platform: platform.name, ...result }; } catch (error) { return { platform: platform.name, code: -1, msg: error.message }; } }); return await Promise.allSettled(uploadPromises); } } /** * 并发控制器 */ class ConcurrencyController { constructor(maxConcurrency = 3) { this.maxConcurrency = maxConcurrency; this.running = 0; this.queue = []; } async add(task) { return new Promise((resolve, reject) => { this.queue.push({ task, resolve, reject }); this.process(); }); } async process() { if (this.running >= this.maxConcurrency || this.queue.length === 0) { return; } this.running++; const { task, resolve, reject } = this.queue.shift(); try { const result = await task(); resolve(result); } catch (error) { reject(error); } finally { this.running--; this.process(); } } } // 创建实例 const instance = new fetchFactory(); // 导出便捷方法 const ketFetch = async (url, options) => { return await instance.request(url, options); }; ketFetch.request = (url, options) => instance.request(url, options); ketFetch.proxy = (url, options) => instance.requestWithProxy(url, options); ketFetch.downloadAll = (urls, options) => instance.downloadConcurrent(urls, options); ketFetch.uploads = (file, platforms, options) => instance.uploadParallel(file, platforms, options); ketFetch.Aborter = () => instance.createController(); ketFetch.fileType = (contentType, url) => instance.detectFileType(contentType, url); ketFetch.toBase64 = (file) => instance.toBase64(file); return ketFetch; })); 结合整个组件权衡一下, 尽量不改动现有逻辑, 不影响现有输出
最新发布
11-23
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值