BitByBit 记账 Vue 版开发记录

使用 svg-sprite-loader 引入 .svg 文件

TypeScript 中如果直接引入 .svg 文件将报错: cannot find module “xxx.svg”

搜索资料,发现需要在 Vue 项目的 shims-vue.d.ts 文件中添加如下配置:

1
2
3
4
declare module "*.svg" {
const content: any;
export default content;
}

但是随后发现引入进来的 .svg 无法显示图标,控制台打印引入的 .svg 文件得到的并不是图标文件,而是一个字符串形式的路径。

这个路径应该可以直接作为 标签的 src 传入,这样引入的就不是图标而是图片了。但是这种方法使得随后修改 .svg 文件源代码更加麻烦,同时图片相对图标有所失真。

为解析时,引入的是文件路径

于是我查阅相关资料,希望可以直接解析出 .svg 源文件。发现缺少 svg-loader 导致webpack无法解析出 .svg 文件。目前有一个较好用的插件: svg-sprite-loader 可以解决这个问题。

1
yarn add svg-sprite-loader -D

官方给出的 webpage.config 配置如下:

1
2
3
4
{
test: /\.svg$/,
loader: 'svg-sprite-loader'
}

配置插件:

1
2
3
4
5
6
7
8
9
10
{
plugins: [
new SpriteLoaderPlugin({
plainSprite: true,
spriteAttrs: {
id: 'my-custom-sprite-id'
}
})
]
}

但是新的问题是 Vue-CLI 并不提供 Webpack.config.js 配置文件,创建的项目下只有一个 vue.config.js 。最后,我选择使用根据 Vue-CLI 的 链式操作 将以上代码翻译成 vue.config.js 接受的代码。修改源文件,例如:.svg 图标颜色也比较方便,直接改 vue.config.js 配置文件即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const path = require('path');

module.exports = {
chainWebpack: config => {
const dir = path.resolve(__dirname, 'src/assets/icons');

// 配置svg-sprite-loder
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}]);

// 其他 svg loader 排除 icons 目录
config.module.rule('svg').exclude.add(dir);

config
.plugin('html')
.tap(args => {
args[0].title = 'BitByBit'
return args
});
}
};

随后 <symbol> 标签被引入 <svg> ,此时便可正常使用 svg-symbol 了:

1
2
import money from "@/assents/icons/money.svg";
console.log(money);
1
2
3
<svg>
<use :xlink:href="#money"/>
</svg>

正常引入

一次性引入所有 .svg 文件

每次引入图标文件都要写一遍 svg-symbol 的代码,十分冗余。经过大量查阅资料,最终我使用以下方法一次性引入了所有文件:

1
2
3
4
5
6
let importAll = (requireContext: __WebpackModuleApi.RequireContext) => requireContext.keys().forEach(requireContext);
try {
importAll(require.context("../assets/icons", true, /\.svg$/));
} catch (error) {
console.log(error);
}

重大 bug:手机端微信浏览器中 window.prompt() 无效

PC 端和手机浏览器端开发测试完毕,当我准备最终发布时,竟发现一个重大 bug :微信手机端浏览器上无法添加标签。经过多端浏览器测试,发现也只有微信浏览器无法添加标签。这说明产生的 bug 不是代码的问题,原因可能是手机微信浏览器的数据库存储 或者 原生 api 不兼容手机微信浏览器。但是由于 Statistics 保存功能可以正常使用,统计图和收支明细也都可以正常显示,问题肯定不是出在 localStorage 上。最终确定JavaScript 原生api 不兼容手机端微信浏览器。经过打断点,发现添加标签时以下代码未执行,这证明了我的推断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export class TagHelper extends Vue {
public createTag() {
const name = window.prompt("请输入自定义标签");
if (name) {
// 此句未执行
this.$store.commit("createTag", name);
} else {
// 此句执行
window.alert("fuck 标签名不能为空");
}
if (this.$store.state.createTagError) {
window.alert(map[this.$store.state.createTagError.message] || "未知错误");
}
}
}

手机端微信浏览器添加标签时,页面跳出待提交提示框,但是并没有被阻塞直接弹出 "fuck 标签名不能为空" ,这说明微信不认识 window.prompt(); ,它拿到的 name 值为 undefined

手机端微信浏览器不认识 window.prompt() 方法

为此,我专门自己封装了 MyPrompt.vue 组件解决了这个 bug。

Vue 实现输入弹框

主要使用 v-if 来实现输入弹框。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<div class="mask" v-if="showModal" @click="showModal=false"></div>
<button @click="showModal=true" class="add">添加</button>
<div class="pop" v-if="showModal">
<div class="prompt">
<div>请输入新增标签名:</div>
<div>
<input
type="text"
placeholder="此处输入标签名"
/>
</div>
<div>
<button @click="showModal=false">提交</button>
</div>
</div>
</div>
</div>
</template>
1
2
3
4
5
6
7
8
<script lang="ts">
import Vue from "vue";
import {Component, Prop} from "vue-property-decorator";
@Component
export default class MyPrompt extends Vue {
showModal = false;
}
</script>

实现新增标签功能

应该可以直接在 MyPrompt 上完成添加标签操作,测试过的确没有问题,控制台并没有报警告。但本项目还是选择:按钮在父组件 Label 和 Tags ,弹框是子组件 MyPrompt ,添加标签使用父子组件传值来实现。

父组件:

1
2
3
4
5
6
7
8
9
<template>
<div>
<MyPrompt
:newName="newName"
@update:newName="addNewName"
@submit:newName="createTag"
/>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script lang="ts">
import Vue from "vue";
import {Component} from "vue-property-decorator";

@Component
export default class Tags extends Vue {
newName = "";
addNewName(value: string) {
this.newName = value;
}

createTag() {
if (this.newName) {
this.$store.commit("createTag", this.newName);
this.newName = "";
} else {
window.alert("标签名不能为空");
}
}
}

</script>

子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<div>
<div class="mask" v-if="showModal" @click="showModal=false"></div>
<button @click="showModal=true" class="add">添加</button>
<div class="pop" v-if="showModal">
<div class="prompt">
<div>请输入新增标签名:</div>
<div>
<input
type="text"
:newName="newName"
placeholder="此处输入标签名"
@input="onAddNewName($event.target.value)"
/>
</div>
<div>
<button
@click="showModal=false;onSubmit()"
class="btn"
>提交
</button>
</div>
</div>
</div>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script lang="ts">
import Vue from "vue";
import {Component, Prop} from "vue-property-decorator";

@Component
export default class MyPrompt extends Vue {
showModal = false;
@Prop({default: ""})
readonly newName!: string;

onAddNewName(value:string){
this.$emit("update:newName",value);
}

onSubmit(){
this.$emit("submit:newName");
}
}
</script>

版权声明:本文作者为「Andy8421」.本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!