vite-plugin-vue2-svg插件开发和总结
March 03, 2021
前言
最近在做旧项目(vue2)的 vite 工程化改造中,遇到了 svg 引入和优化的问题。因为 vite 是相对新的一个工程工具,所以相应的生态也不是特别丰富。目前这个阶段而言,关于可用的 vite svg 工程化库有三个,分别是vite-svg, vite-svg-loader, vite-plgin-svg-sprite。这里面前两个库都是针对 vue3 的,所以排除了,只剩下 vite-plugin-svg-sprite 可选。这个库的功能还是比较齐全的,单 svg svgo 优化,svg sprite,框架无关,算是很不错的选择。介意的地方有几个
模版代码
在调用上,不是特别清晰,需要写一些模版代码,下面是一个比较典型的 svg 引用
<template>
<svg><use :xlink:href="`#${icon}`"></use></svg>
</template>
<script>
import icon from "./icon.svg"
export default {
data() {
return {
icon,
}
},
}
</script>
可以看到,处理引入 svg,还需要写一些模版 template 才可以引用真正的 svg
动态加载
这个是 svg sprite 的通病,因为 svg sprite 需要把所有 svg 全部压在一个文件里面,这样你的网站加载的时候只能一次性把所有需要用到的 svg 加载进来,没办法加载到页面的决定性的资源。我猜测这是大部分网站放弃 svg sprite 的原因。目前比较主流的方法都是将 svg 转换成组件使用的。
vite-plugin-vue2-svg 插件开发
看了这几个插件的源码,发现现阶段没有特别适合 vue2 的方案,所以决定开发一个新的插件vite-plugin-vue2-svg。虽然以前也有一些插件开发和包发布的经验,但是这次决定采取更高的标准去完成这件事。
工具栈
这次选用的组合是 yarn、typescript,以及一些必要的规范工具。
开发
这个插件是参考 vite-svg 完成的。在 compileSvg 的阶段有一些麻烦。这个函数主要的作用是接受一个 svg file 的 content,然后返回 vue component 的 file content 一开始选择纯手撸 render
function compileSvg(svg) {
return `
export default {
render(h) {
h('span', {
domProps: {
innerHTML: '${svg}',
}
})
}
}
`
}
咋一看好像没有什么问题,但是问题很大
- svg 需要由一个 wrapper tag 包裹,这样在调用时一些 dom props 就没办法传递到真正的 svg tag 上面。
- 有 xss 风险,这里将一个 svg 作为 innerHTML 直接插入到 dom 中,如果 svg 文件里面有一些 script 标签,也会一并插入到 dom 中,虽然所有的 svg 都来自内部,但是作为一个插件开发者应该考虑各种状况。
所以看来,得上 vue template compiler 了
import { compile } from "vue-template-compiler";
function compileSvg(svg) {
return compile({
id: ...,
source: svg,
filename:...
}).code;
}
这里又有一个比较麻烦的地方,我使用 vue-template-compiler 编译后,代码中有with(this)
的代码,这样根本没办法在严格模式下运行。果不其然,引用后直接就报错了。
于是我参考了另一个库 vite-plugin-vue2,它里面使用了一个@vue/component-compiler-utils 的包,用于将*.vue 文件编译出 vue component。于是最终代码变成这样
import { compileTemplate, parse } from "@vue/component-compiler-utils";
import * as compiler from "vue-template-compiler";
function compileSvg(svg) {
const template = parse({
source: `
<template>
${svg}
</template>
`,
compiler: compiler,
filename: ...,
}).template;
if (!template) return "";
const result = compileTemplate({
compiler: compiler,
source: template.content,
filename: ...,
});
return `
${result.code}
export default {
render: render,
}
`;
}
整体看上去还是比较简洁的。
打包
一开始是使用 rollup 打包,但是总是发现会将一些外部的包引入到最终代码中,导致运行报错,一开始没意识到这个插件在 node 中运行,其实只需要将 typescript 编译到 javascript 就可以了,后来放弃使用 rollup 包,只使用 tsc 即可。
tsc 配置
tsconfig 打包配置有几个关键的字段是要声明的
{
"compilerOptions": {
"target": "es5", // 这里其实填es6也是可以的,毕竟只是在node中运行
"module": "commonjs", // 会添加额外的runtime将import转换成cjs。虽然有些node开了esmodule实验特性,不过还是转换比较好
"outDir": "./lib", // 这里是输出目录
"declaration": true // 这一步会确定*.d.ts文件会不会输出到目录,没有这个type文件,那你的插件就没办法被正确的类型推断
}
}
调试
以往的调试我都是直接在 src 里面加调试文件和代码做调试的,所以这次参考了其他一些库,更加标准地做这件事。
这里主要是将一个调试用的项目放在/examples 目录下,与 src 文件夹区分开来。examples 下是一个完整的 vite 项目,可以用于测试插件。主要的难点是如何将插件链接到这个项目中。
一开始使用 yarn 本地依赖的方法,在 package.json 中添加"vite-plugin-vue2-svg": "file:../"
,这样 yarn 会将插件的目录链接到调试项目中。但是这里有一个问题,每次修改完代码我都需要重新 build 然后yarn --force
安装依赖,过程比较缓慢。
后来我参考了yarn link
的方案,彻底解放调试效率。
发版
npm 发包需要在 package.json 填写一些必要的字段
{
...
"main": "lib/index.js", // 这里需要指定包入口文件
"types": "lib/index.d.ts", // 这里需要指定types入口
"files": [
"lib"
] // 这个字段比较关键, 我一开始发包的时候,尝试数次都只能将*.js发上去,自动忽略了*.d.ts和sourcemap,指定了这个字段后,该目录下所有文件都会入npm包
...
}
然后执行yarn publish --registry="https://registry.npmjs.org/"
,添加后面 registry 是因为我用的是 taobao 源,所以发版会发不上去,需要将注册源临时换到官方 npm