简介

是我之前写过的一个图片切割器,这是 GitHub 链接,其主要功能是通过 canvas 对图片进行一个切割,从而达到发朋友圈、个人资料大图片的一个效果。

用到的库有:Vue, Pinia, Tailwind CSS

这是我三个月前一晚上加工出来的粗糙产物,所以说有比较多的槽点,也算是新人容易犯的一些错误。

重构点

这里列出几个大的点。注意,尽管有些地方我用的是 HTML 代码块(右上角或左上角显示 HTML),但它们都是 Vue 代码,只是我不想再配置 markdown 引擎罢了。

策略对象的 Store

  • 原写法
1
2
3
4
5
6
7
8
9
10
11
12
13
export const strategies: Record<string, Strategy> = {
qq3x3: {
label: 'QQ个人资料图片3x3',
unit: 0.333333,
scale: 0.75,
steps: [ /* 略 */ ]
},
}

export const useStrategyStore = defineStore('strategy', () => {
const strategy = ref('qq3x3')
return { strategy }
})

这导致用的时候是非常丑,所以说我们需要改进。

  • 改进
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export const useStrategyStore = defineStore('strategy', () => {
const strategy = ref(strategies.qq3x3)
const name = ref('qq3x3')

const strategyName = computed({
get: () => name.value,
set: (value) => {
if (!Reflect.has(strategies, value))
throw new TypeError(`unknown strategy: ${value}`)
name.value = value
strategy.value = strategies[value]
},
})

return { strategy: readonly(strategy), strategyName }
})

这样的话,通过提供一个计算属性 strategyName 和只读的 strategy 策略对象,来避免一些操作失误导致 bug 的出现。

循环渲染

  • 原写法

大概是这个意思,具体的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup lang="ts">
const canvases = [] as HTMLCanvasElement[]
// 此处省略...
</script>

<template>
<div v-for="(step, index) in strategy.steps" :key="index">
<p>{{ step.label }}</p>
<canvas
:ref="el => { canvases[index] = el as HTMLCanvasElement }"
/>
<button @click="handleDownload(index)">
下载
</button>
</div>
</template>

这里把 for 循环渲染里面的内容直接存到数组里了。

  • 改进

我们应该把内部单独抽象成一个组件,这样才合理,而不是用数组来处理一批相同的事物。鉴于每个 canvas 是一个图片片段,这里抽象出一个PhotoFragment.vue(省略除了 props 的其它部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup lang="ts">
const { index } = defineProps<{ index: number }>()
</script>

<template>
<div v-show="image !== undefined" class="flex flex-col space-y-4">
<p>{{ step.label }}</p>
<canvas ref="canvas" />
<Button @click="handleDownload">
下载
</Button>
</div>
</template>

我们既然用 store 传递信息了,就不要在 for 循环上通过 props 传了,这样会造成很多麻烦。

那么再用一个 PhotoResult.vue 渲染 PhotoFragments.vue(省略 ts 部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div class="space-y-4">
<h1 class="text-3xl">
处理结果
</h1>
<PhotoFragment
v-for="(step, index) in strategy.steps"
:key="step.label + index"
:index="index"
/>
<div v-show="image === undefined" class="text-slate-500 font-medium">
暂无数据
</div>
</div>
</template>

这里的 key 我只是觉得,这个组件应该在 step 被更换时也更新一下,所以就把 stepindex 配合起来组成了一个 key,至于是不是非得这么写不可,我还是不太清楚的。

元素底部对齐

  • 原写法

因为我的标题上几个元素大小不一,它们默认是顶部对齐的,而我想要从大到小底部对齐的效果。

因此我一开始使用了设置 padding-top 来解决这一问题。

1
2
3
4
5
6
7
8
9
<div class="flex flex-row space-x-3">
<h1 class="text-3xl">TITLE</h1>

<span class="text-xl pt-2">VERSION</span>

<a class="hidden md:block" href="https://example.com">
<img class="pt-3" src="..." alt="Alt">
</a>
</div>
  • 改进

其实只在父元素上 align-items: flex-end 即可,加一个 tailwind 中的 items-end 类。

1
2
3
<div class="flex flex-row space-x-3 items-end">
...
</div>

这样就不必手动设置 padding-top 了。

不用复杂 props

props 变得太复杂,并且传递起来很麻烦时,就不要用 definePropsdefineEmits 传了。如果这样的话,就要写一堆冗余的代码专门为了传递数据了。

监听文件 onchange 事件

我们监听 onchange 事件,但是需要额外处理。

1
2
3
4
5
6
7
8
9
10
<script setup lang="ts">
const uploadEl = ref<HTMLInputElement>()
async function handleImage() {
// ...
}
</script>

<template>
<input ref="uploadEl" type="file" accept="image/*" class="hidden" @change="handleImage">
</template>

这样做貌似没问题,但是如果我们的切割模式变化了,而选择了同一个文件,这样就会导致 input 没有变化,也就不触发 onchange 事件。

所以我们需要在 handleImage 最后加上一句:

1
uploadEl.value!.value = ''

清空我们选择的文件,这样问题就解决了。

总结

具体的代码大家可以到 GitHub 仓库去查看,链接已经放在文章开头。就这样,拜拜。