如何将外部SVG转换为Vue3组件?(Vite+TS+Script Setup)
问题描述
我有一个基于Vue3、Vite、TypeScript且使用Script Setup的组件项目。目标是从外部存储桶(比如S3)获取SVG图片,作为内联SVG在组件中使用,并且能给SVG设置viewBox、fill等props。内部SVG的实现已经搞定,但外部SVG处理遇到问题:
目前通过fetch获取SVG文本,用DOMParser转成SVG元素,但转成组件传入<component>标签失败;直接追加元素的话,无法通过props编辑属性。现有代码如下:
<template> <div class="svg-box" :style="{ height: props.height + 'px', width: props.width + 'px' }" > <component :is="dynamicSvg" v-if="dynamicSvg" ref="icon" :fill="props.fill" :viewBox="viewBox" :class="name" /> </div> </template>
<script setup lang="ts"> import { ref, onBeforeMount } from 'vue' import type { Ref } from 'vue' interface dynamicSvgProps { name?: string fill?: string width?: number height?: number } const props = withDefaults(defineProps<dynamicSvgProps>(), { name: 'placeholder', }) const dynamicSvg: Ref<string | undefined | null> = ref(null) const icon: Ref<SVGSVGElement | null> = ref(null) const svgBox: Ref<SVGSVGElement | null> = ref(null) const viewBox = '0 0 24 24' const fetchSvg = async (svgName: string) => { let url = `https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/${svgName}.svg` fetch(url) .then((response) => response.text()) .then((svgText) => { const parser = new DOMParser() const svgElement = parser .parseFromString(svgText, 'image/svg+xml') .querySelector('svg') // 直接追加元素无法通过props编辑属性 // svgBox.value.append(svgElement) // 尝试转成组件传入<component>失败 const svgComponent = defineComponent(() => { return { render: () => h('svg', svgElement ) } }) return svgComponent }) } onBeforeMount(async () => { dynamicSvg.value = await fetchSvg('check') }) // 内部SVG的可行方案 const importSvg = async (svgName: string): Promise<string | undefined> => { if (svgName) return await import(`../../assets/icons/${svgName}.svg`) } </script>
解决方案
核心思路
不用把SVG转成组件,而是将解析后的SVG元素转换为Vue虚拟DOM结构,结合h函数渲染,这样就能正常传递props并实现响应式更新。DOMParser是完全可行的解析方案,只需调整处理逻辑适配Vue的渲染机制。
具体实现代码
<template> <div class="svg-box" :style="{ height: `${props.height}px`, width: `${props.width}px` }" > <render-svg v-if="svgData" :svg-data="svgData" :fill="props.fill" :view-box="viewBox" :class="props.name" /> </div> </template> <script setup lang="ts"> import { ref, onBeforeMount, h, defineComponent } from 'vue' import type { Ref } from 'vue' interface DynamicSvgProps { name?: string fill?: string width?: number height?: number } const props = withDefaults(defineProps<DynamicSvgProps>(), { name: 'placeholder', width: 24, height: 24, fill: 'currentColor' }) const svgData: Ref<{ attrs: Record<string, string>, children: any[] } | null> = ref(null) const viewBox = '0 0 24 24' // 将SVG元素解析为Vue虚拟DOM所需的结构 const parseSvgToVNodeData = (svgElement: SVGElement) => { // 提取SVG原生属性 const attrs: Record<string, string> = {} Array.from(svgElement.attributes).forEach(attr => { attrs[attr.name] = attr.value }) // 递归转换子节点为虚拟节点 const children = Array.from(svgElement.childNodes).map(node => { if (node.nodeType === Node.ELEMENT_NODE) { const el = node as Element const childAttrs: Record<string, string> = {} Array.from(el.attributes).forEach(attr => { childAttrs[attr.name] = attr.value }) return h(el.tagName.toLowerCase(), childAttrs, parseSvgToVNodeData(el).children) } return node.textContent || '' }) return { attrs, children } } const fetchSvg = async (svgName: string) => { const url = `https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/${svgName}.svg` const response = await fetch(url) const svgText = await response.text() const parser = new DOMParser() const svgElement = parser.parseFromString(svgText, 'image/svg+xml').querySelector('svg') if (svgElement) { svgData.value = parseSvgToVNodeData(svgElement) } } // 封装SVG渲染组件,合并自定义props与原生属性 const RenderSvg = defineComponent({ props: { svgData: { type: Object, required: true }, fill: { type: String, default: 'currentColor' }, viewBox: { type: String, required: true }, class: { type: String, default: '' } }, render(props) { // 自定义props优先级高于SVG原生属性 const mergedAttrs = { ...props.svgData.attrs, viewBox: props.viewBox, fill: props.fill, class: props.class } return h('svg', mergedAttrs, props.svgData.children) } }) onBeforeMount(async () => { await fetchSvg('check') }) </script>
简易替代方案(无响应式更新)
如果不需要props变化时自动更新SVG属性,可以直接修改SVG文本后用v-html渲染:
<template> <div class="svg-box" :style="{ height: `${props.height}px`, width: `${props.width}px` }" v-html="processedSvg" /> </template> <script setup lang="ts"> import { ref, onBeforeMount } from 'vue' // ... 省略props定义 const processedSvg = ref('') const fetchSvg = async (svgName: string) => { const url = `https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/${svgName}.svg` const response = await fetch(url) let svgText = await response.text() // 替换目标属性 svgText = svgText.replace(/viewBox="[^"]*"/, `viewBox="${viewBox}"`) svgText = svgText.replace(/fill="[^"]*"/, `fill="${props.fill}"`) processedSvg.value = svgText } onBeforeMount(async () => { await fetchSvg('check') }) </script>
关于DOMParser的适用性
DOMParser是浏览器原生API,解析SVG文本完全合适,性能无问题。需要注意外部SVG的CORS配置:如果是S3桶,需在桶权限设置中允许你的域名跨域访问。
内容的提问来源于stack exchange,提问作者Rachel Romée
相关产品推荐
相关产品推荐

