iT邦幫忙

2021 iThome 鐵人賽

DAY 18
0
Modern Web

[ 重構倒數30天,你的網站不Vue白不Vue ] 系列 第 18

[重構倒數第13天] - Vue3定義自己的模板語法

前言

該系列是為了讓看過Vue官方文件或學過Vue但是卻不知道怎麼下手去重構現在有的網站而去規畫的系列文章,在這邊整理了許多我自己使用Vue重構很多網站的經驗分享給讀者們。

我們很常會再 vue 的 template 中使用 v-ifv-show等語法來處理我們的 ui ,但其實 vue 有提供我們一個 API 叫做directive讓我們可以自己定義模板語法,我們就會使用 directive來把一些對 ui 的操作給包起來,方便我們使用。

我們可以看一下官方所提供的這個例子

<div id="simplest" class="demo">
  <input v-focus />
</div>
const app = Vue.createApp({});
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})
app.mount('#simplest')

在這邊我對 directive 去定義一個叫做 focus的語法,然後塞入我們的 vue 裡面,這樣一來我再 template 中就可以用 v-[name]的方式再 html 使用,然後達到我要的效果,所以這邊我用 v-focus 然後綁定再 input 這個 DOM 身上。

再來我們來看 directive的第二個參數,這裡面塞的就是我們的生命週期函式。

以下是我們 directive的生命週期表,在這邊可以看到跟我們的 component 的生命週期很像,但是我們是以掛到DOM上面來作為執行的順序。

官方文件: https://v3.cn.vuejs.org/api/application-api.html#directive

// Vue3 版本
app.directive('my-directive', {
  // 在綁定DOM的 attribute 或事件監聽被使用之前調用
  created() {},
  // 在綁定DOM的父組件掛載之前調用
  beforeMount() {},
  // 綁定DOM的父組件被掛載時調用
  mounted() {},
  // 在包含組件的 VNode 更新之前調用
  beforeUpdate() {},
  // 在包含組件的 VNode 及其子組件的 VNode 更新之後調用
  updated() {},
  // 在綁定DOM的父組件移除之前調用
  beforeUnmount() {},
  // 移除綁定DOM的父組件時調用
  unmounted() {}
})

vue2 directive 的生命週期函式已經被重命命名,所以再升級 vue3 的時候要特別注意一下。

// Vue2 版本
Vue.directive('highlight', {
    // 綁定到DOM後調用。只調用一次。
  	bind() {},
    // DOM插入父組件後調用。
    inserted () {},
    // 當DOM更新,但子組件尚未更新時調用。
    update  () {},
    // 組件和子組件被更新,就會調用。
    componentUpdated  () {},
    // 指令被移除就會調用,也只調用一次。
    unbind  () {},
})

官方文件 : https://v3.vuejs.org/guide/migration/custom-directives.html#overview

我們的 mounted會把它掛載的 DOM 實體給 return回來,這裡回傳是一個 input 的表單DOM,所以我們就可以像一般的 javascript 操作一樣,我執行這個.focus()的函式,讓我一進來這個頁面的時候,將我們滑鼠目標放到這個表單身上。

app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})

官方的 codepen 範例 : https://codepen.io/team/Vue/pen/JjdxaJW?editors=1010

# 已經對 directive 有了概念之後,現在我們來看一些更加實際的使用方式

mike vue3

首先這是一個像是FB貼文的卡片,然後當我拿到資料之後,我就會把內容給一個擺上去,但是你注意看一下時間的地方,下面是API 回傳的格式。

{
    "createdAt": "2021-09-14T04:28:05.885Z",
    "name": "Phyllis Abernathy V",
    "avatar": "https://cdn.fakercloud.com/avatars/amanruzaini_128.jpg",
    "post_date": 1631608299879,
    "photo": "http://placeimg.com/640/480",
    "content": "transmit cross-platform capacitor",
    "id": "1"
},

一切都看起來蠻正常的,但是在時間的地方它是回傳一個 Timestamp ( milliseconds )給你,這其實蠻常見的,不是所有的後端都會幫你轉換時間的部分,所以這時候我們放上去卡片的地方就要寫一個 function 去做轉換時間的動作,在這邊我選擇用 day.js來幫助我轉換時間,體積小功能又強大,沒用過的可以考慮用用看。

Day.js 官網 : https://day.js.org/en/

我們可以用以下的方式來轉換時間格式,所以接下來包裝一下。

dayjs(1631608299879).format('YYYY/MM/DD')

Vue3 mike

<script>
import { ref, onMounted } from "vue";
import axios from "axios";
import dayjs from "dayjs";
export default {
  setup() {
      const postCard = ref([]);
      
      const timestamp = (time) =>{
          return dayjs(time).format('YYYY/MM/DD')
      }

      onMounted(() => {
          axios.get("https://60bd9841ace4d50017aab3ec.mockapi.io/api/post_card").then((res) => {
              postCard.value = res.data;
          });
      });

      return {
          postCard,
          timestamp
      };
  },
};
</script>

<template>
  <div class="card" v-for="card in postCard" :key="card.id">
    <header>
      <img class="avatar" :src="card.avatar" />
      <div>
        <h1>{{ card.name }}</h1>
        <p>
          {{ timestamp(card.post_date) }}
        </p>
      </div>
    </header>
    <p class="content">{{ card.content }}</p>
    <img class="post_photo" :src="card.photo" alt="" />
  </div>
</template>

我寫了一個 function 會回傳 format 之後的時間,然後放到 html 之中timestamp(card.post_date) 給 render 出來,這樣可以把我們的時間做轉換。

But...

如果很多地方都要做這樣的時間轉換,然後我又要一直寫 dayjs(time).format('YYYY/MM/DD'),看起來就不是很理想,所以這邊我要使用 directive來包裝這個 dayjsformat 函式。

首先我的需求是要像下面這樣,我只要使用 v-timeformat就可以把 timestamp 給轉換成我要的格式給我。

<p v-timeformat="card.post_date"></p>

所以接下來我們來註冊一個 timeformat 的模板語法。

import { createApp } from "vue";
import dayjs from "dayjs";
import App from "./App.vue";
const app = createApp(App);

// 先註冊一個 timeformat 的語法
app.directive("timeformat", {
  mounted(el, binding) {
    const time = dayjs(binding.value).format("YYYY年MM月DD日");
    el.innerText = time;
  }
});

app.mount("#app");

因為再 template 這邊我有塞內容 (card.post_date) 進去

<template>
  <div class="card" v-for="card in postCard" :key="card.id">
    <header>
      <img class="avatar" :src="card.avatar" />
      <div>
        <h1>{{ card.name }}</h1>
        <p v-timeformat="card.post_date"></p>
      </div>
    </header>
    <p class="content">{{ card.content }}</p>
    <img class="post_photo" :src="card.photo" alt="" />
  </div>
</template>

所以 mounted除了回傳綁定的 DOM 以外,第二個參數會回傳你傳入的 value,所以我們可以 binding.value的方式取得它的值,當我拿到 value 之後我就執行 dayjs(binding.value).format("YYYY年MM月DD日"),把我的時間給轉換完後,再透過 innerText 給塞入到我們的 DOM 之中,就大功告成了。

codesandbox 完整範例 : https://codesandbox.io/s/zv46o?file=/src/main.js

# 接下來我們來看 directive 的另外一種使用方式

我們很常會製作有很多圖片的頁面,但是圖片的載入是非同步的,所以通常我們會寫一個 load 的 function 來判斷圖片有沒有載入完成,在我們前面的範例也有示範相關的做法,不過那種做法通常都是圖片前面會蓋一個 laoding 的頁面,等到圖片載入完成之後再拿掉 laoding page,這種作法雖然很常見,不過今天我們來介紹一下如何使用 directive 來做到像是圖片的 lazyload 效果。

mike Vue3

首先我希望我的 img 身上可以掛一個 v-src 的語法

<img v-src="https://cdn.fakercloud.com/avatars/popey_128.jpg" />

然後它可以被在背後去載入,載入完成後把圖片給放到 img 標籤身上給 show 出來。

app.directive("src", (el, binding) => {
});

我們先註冊一個名叫 srcdirective,然後第二個參數給它一個 callback 函式,callback 函式???,你可能會問說怎麼不是一個物件,directive的第二個參數總共可以帶兩種不同類型的參數,一個是物件一個是函式,物件就像我們上面一樣,可以有多個生命週期,如果是一個函式的話,這個函式等同於 mounted的階段執行,然後一樣可以回傳綁定的 DOM 還有 value。

app.directive("src", (el, binding) => {
  el.style.opacity = 0;
  if (binding.value) {
    const img = new Image();
    img.src = binding.value;
    img.onload = () => {
      el.src = binding.value;
      el.style.opacity = 1;
    };
  }
});

首先一開始的時候,我先把圖片的透明度設成 0,你可能會問說,怎麼不是 display: none 而是用 opacity = 0呢 ? 原因是因為我的圖片如果有設css的高度的話,設定透明度高度還會在,所以我的版型還不會跑版,再來判斷我的 binding.value 是否有將圖片的路徑給傳入進來,如果有傳入圖片路徑我就去對它執行 onload 看圖片有沒有載入完成,如果載入完成我就把圖片的透明度給變成 1,也就是opacity = 1,這時候可以做一下透明度 0 ~ 1 的補間動態,所以加一下 css3 的 transition就大功告成。

<style>
img {
  transition: opacity 0.3s;
}
</style>

但是如果今天圖片有問題呢 ?

這個時候我就會加一個 onerror 來處理有問題的圖片,我推薦像是下面這種作法。

import errorImg from "./assets/error.png";

app.directive("src", (el, binding) => {
  el.style.opacity = 0;
  if (binding.value) {
    const img = new Image();
    img.src = binding.value;
    img.onload = () => {
      el.src = binding.value;
      el.style.opacity = 1;
    };
    img.onerror = () => {
      el.src = errorImg;
      el.style.opacity = 1;
    };
  }
});

透過 onerror 我們可以確保當圖片出現問題的時候,不會讓畫面直接出現問題,而是我們可以載入預設的圖片,讓畫面至少維持在一個狀態,不會看起來很突兀,所以我先載入了一個 error.png,當圖片出現問題的時候,我就把這個圖片給替換上去,透明度改回 1,這樣一來畫面上面就可以看到我們預先準備好的圖片了。

Vue mike

codesandbox 完整範例 : https://codesandbox.io/s/oulpd?file=/src/main.js

最後

所以在開發上面不是所有的共用邏輯都需要使用 composition api ,我們可以依照自己的需求來決定使用那些功能來幫助我們開發,像是把第三方套件結合自己定義的模板語法來產生最後結果的做法,或是自己做一個 lazyload 的效果也是一種不錯的選擇,當然能用 directive的地方還有很多! 不過今天就先到一段落吧,我們明天見囉。

QRcode

那如果對於Vue3不夠熟的話呢?

Ps. 購買的時候請登入或註冊該平台的會員,然後再使用下面連結進入網站點擊「立即購課」,這樣才可以讓我獲得更多的課程分潤,還可以幫助我完成更多豐富的內容給各位。

我有開設了一堂專門針對Vue3從零開始教學的課程,如果你覺得不錯的話,可以購買我課程來學習
https://hiskio.com/packages/AYR5m7VR3

那如果對於JS基礎不熟的朋友,我也有開設JS的入門課程,可以參考這個課程
https://hiskio.com/packages/Q9R4OYoyD

訂閱Mike的頻道享受精彩的教學與分享

Mike 的 Youtube 頻道
Mike的medium
MIke 的官方 line 帳號,好友搜尋 @mike_cheng


上一篇
[重構倒數第14天] - Vue3處理動態效果(二)
下一篇
[重構倒數第12天] - Vue3 directive 與 Skeleton 實戰組合應用
系列文
[ 重構倒數30天,你的網站不Vue白不Vue ] 31

尚未有邦友留言

立即登入留言