vite3 + vue3如何封装健壮的【SVG插件】?

前言

小伙伴们好,我是代码诗人_,今天带大家用如何用vue3 + vite3封装一个健壮的svg插件!

首先得知道为什么要使用svg

  • 概念:SVG是一种基于XML的矢量图形格式,SVG 与诸如 DOM 和 XSL 之类的 W3C 标准是一个整体,用于在Web和其他环境中显示各种图形;它允许我们编写可缩放的二维图形,并可通过CSS或JavaScript进行操作。它由万维网联盟制定,是一个开放标准。
  • 优点可伸缩性,响应性,交互性,可编程性,体积小性能高
    因为SVG是基于矢量的,所有在放大图形时不会出现任何降低或丢失保真度的情况。它只是重新绘制以适应更大的尺寸,这使得它非常适合多语境场景,例如响应式Web设计。而且性能也极佳,与栅格图形(如GIF,JPG和PNG)相比,SVG图形通常是较小的文件。
  • 浏览器支持度:svg对市面上常见的浏览器已经有很好的支持度

vite3 + vue3如何封装健壮的【SVG插件】?_第1张图片

**所以,综上所述,日常开发中使用svg来代替图片及图标是我们的不二选择!**

# 为什么要封装
如果对svg不封装,代码必定会难以维护,阅读性极差,比如:

vite3 + vue3如何封装健壮的【SVG插件】?_第2张图片
再看下封装后的代码效果:

vite3 + vue3如何封装健壮的【SVG插件】?_第3张图片

一个自定义标签组件即可搞定,也方便维护和阅读

 

下面我们就开始编写插件及组件,来实现它,分5步走

  • 第一步: 建立所需文件
  • 第二步:封装转换并读取svg文件的插件
  • 第三步:利用vite transformIndexHtml封装渲染svg内容
  • 第四步:封装vue组件并全局注册
  • 第五步:页面中应用传值

    第一步:建立所需文件

    首先在src文件夹下建立如下文件:

vite3 + vue3如何封装健壮的【SVG插件】?_第4张图片

目录文件解释

  • SvgIcon/index.vue 封装组件
  • svg目录存放你的svg格式的文件
  • index.js 全局注册svg的钩子
  • svgTagView.js svg插件

第二步:封装(转换并读取svg)文件的插件

1. 首先在svgTagView.js中引入node文件系统,并在vite中使用

import { readFileSync, readdirSync } from 'fs'
  • readdirSync:同步 readdir().返回文件数组列表
  • readFileSync:同步读取文件内容

还不知道node fs文件系统的小伙伴,请点击 传送门

然后在vite.config.js中的插件plugins中使用它

import { svgTagView } from './src/icons/svgTagView.js'
 plugins: [
        vue(),
        svgTagView()
      ]

2. 设置svg的正则匹配标签及属性

const attributeMatch = {
  svgTag: /+].*?)>/, //匹配多个

还不知道svg属性viewbox(svg视区,就是在svg上截取一小块,放大到整个svg显示)的小伙伴,请点击 传送门

3. 递归的形式读取svg文件信息及文件列表

递归svg文件,那么现在就用到了readdirSyncreadFileSync;

(1. 首先我们用readdirSync获取文件列表信息
const svgRes = [] //
let dir = './src/icons/svg/', //存放svg文件的目录
  const dirents = readdirSync(dir, {
    withFileTypes: true // 将文件作为fs.Dirent对象返回,
  })
  console.log(dirents) // 文件列表信息

fs.Dirent 是目录项(可以是文件或目录中的子目录)的表示,通过读取 fs.Dir 返回。 目录项是文件名和文件类型对的组合。

dirent会返回icons/svg下的文件列表信息

启动项目,在终端中会看到文件列表信息,这是我的svg文件信息:

vite3 + vue3如何封装健壮的【SVG插件】?_第5张图片

可以清楚地看到 文件名称name和独立的文件 [Symbol(type)]

(2. 遍历文件列表信息,转换svg

遍历文件获取内容需要递归,所以我们将第一步获取文件信息的代码放到一个函数中,
readFileSync读取文件内容,并用repalce方法配合第一大步的正则逐步匹配转换svg文件,
最后得到一个含有标签的字符串数组

vite3 + vue3如何封装健壮的【SVG插件】?_第6张图片
实现:

function recursivelyFindSvg(dir) {
  const svgRes = []
  const dirents = readdirSync(dir, {
    withFileTypes: true // 将文件作为fs.Dirent对象返回,
  })
  
  for (const dirent of dirents) {
    if (dirent.isDirectory()) { // 判断是否一个目录
      svgRes.push(...recursivelyFindSvg(dir + dirent.name + '/')) //递归获取svg文件内容
    } else {
      const svg = readFileSync(dir + dirent.name)
        .toString()
        .replace(attributeMatch.clearReturn, '')
        .replace(attributeMatch.svgTag, ($1, $2) => {
          let width = 0
          let height = 0
          let content = $2.replace(attributeMatch.clearWidthHeight, (s1, s2, s3) => {
            if (s2 === 'width') {
              width = s3
            } else if (s2 === 'height') {
              height = s3
            }
            return ''
          })
          if (!attributeMatch.hasViewBox.test($2)) {
            content += `viewBox="0 0 ${width} ${height}"`
          }
          return ``
        })
        .replace('', '')
      svgRes.push(svg)
    }
  }
  console.log(svgRes)
  return svgRes
}

运行此段代码,可以在终端看到如下信息:

vite3 + vue3如何封装健壮的【SVG插件】?_第7张图片
第二大步就算完成了

第三步:利用vite transformIndexHtml封装渲染svg内容

上面第二步获取了svg的容,现在我们要将这些内容渲染到视图上,
那么就用到了vite的transformIndexHtml

1. 解释下 transformIndexHtml:

transformIndexHtml是转换 index.html 的专用钩子。钩子接收当前的 HTML 字符串和转换上下文。上下文在开发期间暴露ViteDevServer实例,在构建期间暴露 Rollup 输出的包。

这个钩子可以是异步的,并且可以返回以下其中之一:

  • 经过转换的 HTML 字符串
  • 注入到现有 HTML 中的标签描述符对象数组({ tag, attrs, children })。每个标签也可以指定它应该被注入到哪里(默认是在  之前)
  • 一个包含 { html, tags } 的对象

    2. transformIndexHtml渲染svg

利用transformIndexHtml配合replace替换,并将第二步的svg数组内容放到
svg标准定义的标签中

标准svg标签:

实现:

const svgTagView = (path = './src/icons/svg/', perfix = 'icon') => {
  if (path === '') return
  idPerfix = perfix
  const res = recursivelyFindSvg(path)
  return {
    name: 'svg-transform',
    transformIndexHtml(html) { // transformIndexHtml    转换 index.html 的专用钩子,vite3 独享
      return html.replace(
        '',
        `
            
              ${res.join('')}
            
        `
      )
    }
  }
}

到这里插件就算封装完了,然后我们需要将svg路径写入vite.config.js中,便于全局配置

svgTagView('./src/icons/svg/')

附上完整代码如下:

import { readFileSync, readdirSync } from 'fs'

const attributeMatch = {
  svgTag: /+].*?)>/,
  clearWidthHeight: /(width|height)="([^>+].*?)"/g,
  hasViewBox: /(viewBox="[^>+].*?")/g,
  clearReturn: /(\r)|(\n)/g
}
let idPerfix = ''

function recursivelyFindSvg(dir) {
  const svgRes = []
  const dirents = readdirSync(dir, {
    withFileTypes: true // 将文件作为fs.Dirent对象返回,
    //fs.Dirent 是目录项(可以是文件或目录中的子目录)的表示,通过读取 fs.Dir 返回。 目录项是文件名和文件类型对的组合。
  })
  // console.log(dirents) // { name: 'guanyuwomeng1.svg', [Symbol(type)]: 1 }
  for (const dirent of dirents) {
    if (dirent.isDirectory()) { // 判断是否一个目录
      svgRes.push(...recursivelyFindSvg(dir + dirent.name + '/')) //递归获取svg文件内容
    } else {
      // 读取文件信息并用repalce方法,利用第一步的正则匹配转换svg文件
      const svg = readFileSync(dir + dirent.name)
        .toString()
        .replace(attributeMatch.clearReturn, '')
        .replace(attributeMatch.svgTag, ($1, $2) => {
          let width = 0
          let height = 0
          let content = $2.replace(attributeMatch.clearWidthHeight, (s1, s2, s3) => {
            if (s2 === 'width') {
              width = s3
            } else if (s2 === 'height') {
              height = s3
            }
            return ''
          })
          if (!attributeMatch.hasViewBox.test($2)) {
            content += `viewBox="0 0 ${width} ${height}"`
          }
          return ``
        })
        .replace('', '')
      svgRes.push(svg)
    }
  }
  // console.log(" ~ file: svgTagView.js:28 ~ recursivelyFindSvg ~ svgRes", svgRes)

  return svgRes
}

export const svgTagView = (path, perfix = 'icon') => {
  if (path === '') return
  idPerfix = perfix
  const res = recursivelyFindSvg(path)
  return {
    name: 'svg-transform',
    transformIndexHtml(html) { // transformIndexHtml    转换 index.html 的专用钩子,vite3 独享
      return html.replace(
        '',
        `
          
            
              ${res.join('')}
            
        `
      )
    }
  }
}

插件是封装完了,下一步我们就得封装svg vue组件,来渲染证实它

第四步:封装vue组件并全局注册

打开我们第一次新建的文件components/SvgIcon/index.vue

1. 封装一个全局通用的svg-icon组件如下:

可自行配置svg标签可有的属性(width,class,name等等...),封装语法很简单,这里不再赘述





2. 全局注册上一步封装的svg-icon

打开第一次新建的icons/idnex.js
使用install钩子在vue实例上注册它,如下:

import SvgIcon from '@/components/SvgIcon/index.vue'

export default {
    install: (app) => {
        app.component('svg-icon', SvgIcon)
    }
}

然后在入口文件main.js use使用它

import { createApp } from 'vue'
import App from './App.vue'
import SvgIcon from './icons/index'

const app = createApp(App)
app.use(SvgIcon)
app.mount('#app')

全局注册和use使用搞定,下一步我们在模板中使用它()

第五步:页面中应用组件传值