简介
是我之前写过的一个图片切割器,这是 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
被更换时也更新一下,所以就把 step
和 index
配合起来组成了一个 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
变得太复杂,并且传递起来很麻烦时,就不要用 defineProps
和 defineEmits
传了。如果这样的话,就要写一堆冗余的代码专门为了传递数据了。
监听文件 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 仓库去查看,链接已经放在文章开头。就这样,拜拜。