某天,你在寫的專案,需求說想要之前有做的一個小功能,你做出來但被打槍說跟以前的有點不一樣,以前的比較好,於是你想要使用複製貼上大法(我就爛),但你發現,你不知道從何處開始複製起…
我對通用組件的想像
可複用
都把它拆成組件了,不外乎就是想要他在未來的某一天,成為拯救你專案時程的完美配方。
如何讓組件在不同的地方都可以正常運行,並且在各種情形下都可以掌握他的運行方式。
可維護
需求天天變,程式天天改,讓工程師可以放心跟 PM 說出 沒問題,這改很快,具備可快速更改的程式碼我相信是各位夢寐以求的項目吧。
藉由props、slot去控制該組件本身的功能及呈現,在統一的規格上套上了可客製化的項目。
低耦合
我想要的只是一張顯卡,而不是要買到一整組的電腦RRRR。
通用型的組件一大特點,幾乎沒有業務邏輯,在使用時也不會影響到業務流程,可以做為祖傳組件一路流傳下去。
職責單一
通用組件能夠控制的只有他自己的東西,不管是資料的儲存,或是狀態的切換,基本上都只能改變自己,不屬於自己的東西,就必須傳遞出去讓其他人(e.g. 含有業務邏輯的組件) 做處理。
這些特點看下來,其實通用型組件的成形不會太大,也因為沒有業務邏輯,所以不會是流程中的一部份,沒有他不會怎樣,但有了他,使用者體驗會更好,以下會以常見的複製功能來做示範。
複製功能通用組件
此為用 vue3 所製作
所要達成的功能
- 複製項目 (主要功能)
- 複製網址 (主要功能)
- 可以更換複製的圖示或文字 (客製化)
- 已經複製成功顯示 (增加使用者體驗)
複製組件的畫面
要讓使用者知道這邊可以複製,促使使用者可以點擊他,另一方面,不同地方提醒使用者可以複製的畫面不盡相同,有的是有圖示,有的是一串文字本身就可以點擊,於是這邊就需要用slot去讓外層可以自行設計他的畫面,組件本身只負責點擊,以及實作複製時所需要的input。
<div id="copied" @click.stop="handleCopy"> <slot></slot><!-- 提供給父層客製化 --> <input class="hidden" ref="copiedInput" readonly /> </div>
複製時所需要的資料
組件需要知道要複製得資料,以及想要客製化複製成功時的文字敘述等。
const props = defineProps({ // 要複製的項目 target: { type: String, default: '', }, // 複製項目的名稱 targetName: { type: String, default: '網址', }, // 複製修飾文字 text: { type: String, default: '已複製', }, })
點擊事件
最重要的功能,在使用者點擊下去時要做的動作,流程大概是
- 將 props 的 target 資料放到 input,target 沒有資料就放入該頁的網址
- 運用 document 的複製功能將其資料複製
- 顯示客製化的複製成功提示告知使用者複製成功
const copiedInput = ref(null) const handleCopy = () => { copyUrl() } const copyUrl = () => { const _input = copiedInput.value _input.value = props.target || window.location.href _input.select() _input.setSelectionRange(0, 99999) document.execCommand('copy') showToast() } const showToast = () => { alert(`${props.targetName}${props.text}`) }
最後隱藏HTML的Input
因為 html 中的 input 只有給JS做複製使用,使用者並不需要知道他的存在,畫面也不需要給他位置,於是需要寫一個隱藏的css把它藏起來。
#copied { position: relative; .hidden { opacity: 0; right: 200vw; position: absolute; pointer-events: none; } }
結語
通用組件其實都只是小元件,用途大多是提升使用者體驗,像是複製、圖片顯示、scroll bar客製化等,既可以讓網站變得更好看,又不會破壞整體的統一性,更重要的,可以重複使用,節省大量工時,讓工程師可以專注在其他困難的業務邏輯上,這麼划算的事情,何樂而不為呢?
源碼
<script setup lang="ts"> import { ref } from 'vue' const props = defineProps({ // 要複製的項目 target: { type: String, default: '', }, // 複製項目的名稱 targetName: { type: String, default: '網址', }, // 複製修飾文字 text: { type: String, default: '已複製', }, }) const copiedInput = ref(null) const handleCopy = () => { copyUrl() } const copyUrl = () => { const _input = copiedInput.value _input.value = props.target || window.location.href _input.select() _input.setSelectionRange(0, 99999) document.execCommand('copy') showToast() } const showToast = () => { alert(`${props.targetName}${props.text}`) } </script> <template> <div id="copied" @click.stop="handleCopy"> <slot></slot> <input class="hidden" ref="copiedInput" readonly /> </div> </template> <style lang="scss" scoped> #copied { position: relative; .hidden { opacity: 0; right: 200vw; position: absolute; pointer-events: none; } } </style>