Vue@cli引入svg坎坷多多
一、引入svg的简单方法
在iconfont上选好图标,然后生成一个svg.js, 把这个svg.js保存到项目里,然后引用,就可以使用svg了
代码示例:
Icon.vue
<template>
<svg class="icon" @click="$emit('click', $event)">
<use :xlink:href="`#i-${name}`"></use>
</svg>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component, Prop} from 'vue-property-decorator';
import './svg'
@Component
export default class Icon2 extends Vue {
@Prop(String) name: string | undefined;
}
</script>
上面这种方法虽然简单,但是又有点麻烦,因为当我们需要更新svg图标,就需要重新登录iconfont网站,获取新的svg.js, 然后下载下来,再引入项目,能不能让更新svg简单一些?
答案是可以的。所以我们的目标是:通过webpack来引入svg, 如果有新的svg, 直接把下载好的svg复制到对应的icon文件夹, 然后在任何一个组件里写<Icon name="svgname"></Icon>就可以用这个svg.
接下来将一步步实现这个目标
二、如何用Vue@cli引入svg?
2.1 安装svg loader
我们可以很快地搜索到需要安装svg-sprite-loader, 才能在webpack里使用
但问题是,安装过后,需要配置,但官方文档里的配置是写在webpack.config.js, 而我们的项目是用Vue@cli搭建的,并没有webpack.config.js这个文件,只有vue.config.js
2.2 配置vue.config.js
所以我们需要做的就是把官方文档里 webpack.config.js 关于svg-sprite-loader 的配置 翻译成 vue.config.js 里的配置。于是我就去读了vue的官方文档,发现可以在vue.config.js里用一个chainWebpack来修改webpack的配置
Vue官方给的例子是:
// vue.config.js
module.exports = {
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.loader('vue-loader')
.tap(options => {
// 修改它的选项...
return options
})
}
}
于是照猫画虎,配置写出来我们需要的vue.config.js
const path = require('path')
module.exports = {
lintOnSave: false,
chainWebpack: config => {
const dir = path.resolve(__dirname, 'src/assets/icons') // 当前目录
config.module
.rule('svg-sprite')
.test(/.svg$/)
.include.add(dir).end() // 只包含icons目录
.use('svg-sprite-loader').loader('svg-sprite-loader')
.options({extract: false}).end() //不需要解析成文件
config.plugin('svg-sprite').use(require('svg-sprite-loader/plugin'), [{plainSprite: true}])
config.module.rule('svg').exclude.add(dir) // 其他svg loader排除icons目录
}
}
这样配置完成之后,我们就可以在项目里引用svg了
<svg>
<use x:link:href="对应的svg图标的name"></use>
</svg>
2.3 给svg去色
写MoreMoney记账时,有一个需求是高亮svg图标,即本身灰色的svg图标高亮成黄色或者红色,但在写CSS时,我发现有些svg自己带了颜色,无法修改,怎么办呢?
我去打开svg文件,读了读里面的内容,发现一个属性叫fill, 表示填色。于是进一步在控制台里做实验,发现如果去掉svg里的fill属性,那么这个svg就没有自带颜色了。
但问题是,这么多svg图标,难道我们要一个一个地去删除fill属性?
当然不可能。通过搜索,我发现了一个Loader叫 'svgo-loader', 它可以去除fill标签, 配置如下
config.module
.use('svgo-loader').loader('svgo-loader') //
.tap(options => ({...options, plugins: [{removeAttrs: {attrs: 'fill'}}]})).end()
于是现在我们的vue.config.js是:
const path = require('path')
module.exports = {
lintOnSave: false,
chainWebpack: config => {
const dir = path.resolve(__dirname, 'src/assets/icons') // 当前目录
config.module
.rule('svg-sprite')
.test(/.svg$/)
.include.add(dir).end() // 只包含icons目录
.use('svg-sprite-loader').loader('svg-sprite-loader')
.options({extract: false}).end() //不需要解析成文件
.use('svgo-loader').loader('svgo-loader')
.tap(options => ({...options, plugins: [{removeAttrs: {attrs: 'fill'}}]})).end()
config.plugin('svg-sprite').use(require('svg-sprite-loader/plugin'), [{plainSprite: true}])
config.module.rule('svg').exclude.add(dir) // 其他svg loader排除icons目录
}
}
这样就可以顺利引入svg且给svg去色
2.4 抽取Icon组件
如果每次引用svg都要写一次:
<svg>
<use x:link:href="对应的svg图标的name"></use>
</svg>
实在是有些麻烦,于是我们可以抽取出一个Icon组件,每次<Icon name="svg图标对应的Name"></Icon>这样就好。 Icon组件的实现很简单,就是把本身的代码包裹一层,然后接收一个Name作为参数
Icon组件:
<template>
<svg class="icon" @click="$emit('click', $event)">
<use :xlink:href="`#i-${name}`"></use>
</svg>
</template>
2.5 一次性引入所有svg文件
目前还存在的问题是我们是一个一个地引入svg图标,能不能一次性引入一整个svg文件夹?
可以!用一个ImportAll 方法即可
const importAll = (requireContext: __WebpackModuleApi.RequireContext) => requireContext.keys().forEach(requireContext);
try {
importAll(require.context('../assets/icons', true, /.svg$/));
} catch (error) {
console.log(error);
}
现在我们解决了全部的问题,如果有新的svg, 就把它复制到assets/icons目录下,然后在项目里直接<Icon name="svg图标对应的Name"></Icon>就可以愉快地用它了!
抽取公共组件
在写代码时一直牢记:我与重复不共戴天
在目前写的两个页面里,抽取了一些公共组件:
-
Tag
-
Type
-
Input
-
Button
遇到的问题有:
- 需要传的参数不同,
- 用props 父传子
- 用slot ,
比如Input组件 在money页的placeholder是“写点备注吧”
在editLabel页的placeholder是“限三个汉字”
做法是用prop 传递 placeholder变量
EdiltLabel
<FormItem isNewTag="+" placeholder="限3个汉字或5个英文字母..."
@update:tagName="onTagNameChange"
></FormItem>
formItem
<input :value="value"
@input="onValueChange($event.target.value)"
type="text"
:placeholder="placeholder">
slot:
在labels页,Button是新建标签
在editLabel页, Button是保存标签
Button.vue
<button class="button" @click="$emit('click', $event)">
<slot></slot>
</button>
label.vue
<Button>新建标签</Button>
editLabel.vue
<Button>保存新标签</Button>
- 在不同位置的css不同:用props传一个参数进去,然后用该参数判断是否增加一个class
:class = 'a===1? 'newClass originalClass' : 'originalClass''
css不一样,
所以就传递一个变量进去,通过这个变量判断当前组件用在哪一个页面,然后增加一个class修改css
- 点击事件传递
以input和button为例
EditLabel 里有 FormItem 组件, FormItem组件里有Input
EditLabel传递value进去
<FormItem isNewTag="+"
placeholder="限3个汉字或6个英文字母..."
:value="tag.name"
@update:tagName="onTagNameChange"></FormItem>
FormItem 接受 value 参数
<div :class="isNewTag === '+'? 'notes-wrapper notes-wrapper-no-bg' : 'notes-wrapper '">
<label class="notes">
<input :value="value"
@input="onValueChange($event.target.value)"
type="text"
:placeholder="placeholder">
</label>
</div>
@Prop({default: ''}) value: string | undefined;
监听input事件,如果输入框内容变化,就调用onValueChange函数,把当前的值传给上层组件
onValueChange(value: string) {
if (this.isNewTag === '+') { // 如果在修改标签页
this.$emit('update:tagName', value);
} else { // 如果在money页记账,触发更新value事件
this.$emit('update:value', value);
}
}
这样的话, 实现了由父组件管理用户输入的内容, 子组件里的Input只是作为一个中间站,如果有数据更新,就立刻通知父组件
关于Button
如果直接在Button上设置点击事件会发现无法触发,因为用户点击的是里面的button, 而不是外面包裹的Button组件
解决思路是 当用户点击button时,button就通知Button用户需要做事情了
解决方法是 在button的click事件上触发click事件,通知Button,
Button.vue
<button class="button" @click="$emit('click', $event)">
<slot></slot>
</button>
Editabel.vue
<Button @click="saveTag">保存新标签</Button>
这时,里面的button被点击时,会立刻触发外面Button的点击事件,然后外面Button监听到之后,就执行saveTag保存新标签
代码预览链接:fjliang56.github.io/morney-webs…
继续记录自己第一次做项目碰到的问题和解决方法