[重构倒数第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/bundles/9WwPNYRpz?s=tc

那如果对於JS基础不熟的朋友,我也有开设JS的入门课程,可以参考这个课程
https://hiskio.com/bundles/b9Rovqy7z?s=tc

订阅Mike的频道享受精彩的教学与分享

Mike 的 Youtube 频道
Mike的medium
MIke 的官方 line 帐号,好友搜寻 @mike_cheng


<<:  [Day 03] 一声探气,索性来资料分析 (探索性资料分析)

>>:  Kotlin Android 第13天,从 0 到 ML - Activity 和 Activity 生命周期

Swift纯Code之旅 Day28. 「新增闹钟功能(1) - Struct使用、取得UIDatePicker值」

前言 如果只有画面像的话,那也太弱了吧! 赶紧来实作新增闹钟的功能,做完拿去炫耀给边身边的人看! 实...

【Day4】Navigation导航X注册画面X Firebase Auth

好的,中秋节连假第二天,大家是吃烤肉吃的不要不要的阿? 那我们今天主要要做的就是关於登入页面。今天...

CSS微动画 - 弹出来的选单 Part.2

Q: 是不是来点icon比较知道这是干嘛的? A: 不复杂的可以用css画,复杂的可以考虑出图或是...

CSS文字样式相关属性(DAY11)

今天这篇文章会介绍CSS文字大小、文字粗细、字体和字型,这些都是有关文字样式的相关属性: 文字大小 ...

尚气与十环传奇

尚气与十环传奇在线观看 漫威影业荣誉出品史诗冒险《尚气与十环传奇》,结合前所未见的震撼性动作、令人惊...