• בלוג
  • תרגומון Vue: שלוש דרכים לכתוב קומפוננטה

תרגומון Vue: שלוש דרכים לכתוב קומפוננטה

20/11/2021

המעבר ל Composition API ואחריו ל script setup עלול להיות מבלבל כשמתחילים לעבוד ב Vue: דוגמאות באינטרנט יכולות להופיע בכל אחד משלושת הכתיבים ואפילו התיעוד הרשמי לא בחר המלצה ברורה. כך יוצא שמצד אחד כשמתחילים ללמוד את הפריימוורק אנחנו צריכים לבחור כתיב מסוים, אבל לאורך הדרך בכתיבה אם לא ניפתח להכיר את שני הכתיבים האחרים יהיה לנו מאוד קשה.

בדיוק בשביל זה אני רוצה להציע תרגומון מהיר בין שלושת הכתיבים - בהמשך הפוסט תמצאו את אותה קומפוננטה כתובה שלוש פעמים, כל פעם בגישה אחרת, וכך תוכלו לראות את הדומה והשונה בין הגישות ואני מקווה שגם להרגיש יותר בנוח לתרגם ביניהן.

1. גישה 1 ויו קלאסי

בשביל התרגומון שלי אני רוצה לכתוב קומפוננטה של Counter שמציגה כפתור ועליו מספר הפעמים שלחצו עליו, ומדליקה גם שעון כך שכל שניה שעוברת באופן אוטומטי ירד 1 מהערך. תיבול אחרון הוא להוסיף אפשרות לקוד חיצוני להעביר את הערך הראשוני של ה Counter ובכמה להגדיל את הערך בכל לחיצה בתור Properties.

בכתיב הקלאסי של Vue אני כותב את הקומפוננטה בקובץ עם סיומת Vue. בלוק הסקריפט בקובץ צריך לייצא אוביקט שיגיד ל Vue איך ליצור את הקומפוננטה באמצעות מתודות עם שמות מיוחדים ש Vue יודע להסתכל עליהם. הנה הקוד:

<script>
export default {
  data() {
    return {
      value: this.start,
    }
  },

  props: {
    start: Number,
    step: Number,
  },

  methods: {
    inc() {
      this.value += this.step;
    }
  },

  mounted() {
    this.timer = setInterval(() => {
      this.value -= 1;
    }, 1000);
  },

  unmounted() {
    clearInterval(this.timer);
  }

};
</script>

<template>
  <div>
    <button @click="inc">{{value}}</button>
  </div>
</template>

<style scoped>
</style>

כל המפתחות באוביקט הם שמות שמורים של Vue:

  1. המפתח data מגדיר משתנים ריאקטיביים שקוד הטמפלייט יוכל להשתמש בהם.

  2. המפתח props מגדיר איזה מאפיינים יגיעו מבחוץ בתור Properties.

  3. המפתח methods מגדיר פונקציות שקוד הטמפלייט יוכל להשתמש בהן.

  4. המפתחות mounted, unmounted נקראים Lifecycle Events והם מאפשרים להריץ קוד אוטומטית כשקורה משהו - למשל כשקומפוננטה נכנסת למסך או יוצאת מהמסך (ויש עוד הרבה מהם).

בגלל שכל פעולה נכתבת בפונקציה נפרדת, באופן אוטומטי המשתנה המיוחד this מתיחס לאוביקט Vue ש"מאחד" את כל הגישה לקומפוננטה. בפונקציה inc השתמשתי ב this.value כדי לגשת למשתנה value שהוגדר בתוך data. בפונקציה unmounted השתמשתי ב this.timer שהוגדר בפונקציה mounted. בקיצור כל המתודות המיוחדות יכולות לשמור מידע על אותו this. גם ויו עצמו ישמור עליו מידע, למשל הוא ישמור עליו את כל השדות באוביקט data ואת כל ה Properties שעברו לקומפוננטה.

2. גירסה 2 - מעבר ל Composition API

בעיה אחת שקל לראות עם הכתיב הקלאסי היא שצריך מנגנון מיוחד כדי לשתף "לוגיקה הקשורה ל State" בין מספר קומפוננטות. לוגיקה הקשורה לסטייט זה לוגיקה שמורכבת גם מפונקציות, גם משדות מידע ב data וגם מאירועי מחזור חיים. לדוגמה אם הייתי רוצה לקחת את הרעיון של אינטרוול שכל שניה מוריד ערך של משתנה באחד ולהשתמש בו בקומפוננטה נוספת, אז בכתיב הקלאסי לא ברור איך אני יכול לקחת את החלק הזה בהתנהגות של הקומפוננטה החוצה מהקומפוננטה ולהשתמש בו במקום אחר.

היו כל מיני דרכים להתמודד עם הסיפור הזה לאורך השנים אבל הדרך שהתייצבה בתור פיתרון מומלץ נקראת Composition API. הרעיון שלה הוא לתת לנו יותר שליטה על מה שקורה ביצירת הקומפוננטה ולהפוך את כל המפתחות המיוחדים שראינו לפונקציות שאנחנו צריכים לקרוא להן בצורה יזומה. נקודת הכניסה שלנו לקומפוננטה תהיה עכשיו פונקציה בודדת בשם setup.

גם ב Composition API עדיין משתמשים במפתח מיוחד בשם props בשביל להגדיר את המאפיינים שאנחנו מקבלים מבחוץ, אבל כל שאר ההתנהגות נכנסת לתוך קוד שאנחנו מריצים בפונקציה שנשמרת במפתח המיוחד setup.

הקוד נראה כך:

<script>
import { ref, reactive, onMounted, onUnmounted } from 'vue';

export default {
  props: {
    start: Number,
    step: Number,
  },
  setup(props) {
    const data = reactive({ value: props.start });
    const inc = function() {
      console.log(data);
      data.value += props.step;
    }
    let timer;

    onMounted(() => {
      timer = setInterval(() => {
        data.value -= 1;
      }, 1000);
    });

    onUnmounted(() => {
      clearInterval(timer);
    });

    return {
      data,
      inc,
    };
  }
};
</script>

<template>
  <div>
    <button @click="inc">{{data.value}}</button>
  </div>
</template>

אוקיי אז בספירת שורות מצבנו לא מאוד שונה אבל הקוד כן נראה אחרת, לפחות חלקית:

  1. מפתח props לא השתנה, אבל כל המפתחות האחרים באוביקט המיוצא הוחלפו במפתח אחד בשם setup.

  2. הפונקציה setup מקבלת את ה props בתור פרמטר ומבצעת את כל מה שהמפתחות המיוחדים מהגירסה הקודמת היו עושים.

  3. בשביל להגדיר את data שעליו נשמר המידע הריאקטיבי השתמשתי בפונקציה של ויו שנקראת reactive. אגב עכשיו אנחנו יכולים לשמור מספר אוביקטי data באותה קומפוננטה.

  4. הפונקציה inc יכולה לגשת למשתנים הריאקטיביים של data בזכות JavaScript Closures.

  5. המפתחות mounted ו unmounted הפכו לפונקציות onMounted ו onUnmounted, כל אחת מהן מקבלת כקלט פונקציה. שוב זה מלהיב כי עכשיו אנחנו יכולים להוסיף כמה קודי טיפול לכל אירוע (כלומר אפשר לקרוא ל onMounted כמה פעמים עם דברים שונים שצריך לעשות). גם כאן הגישה למשתנים משותפים נעשית באמצעות JavaScript Closures ולא צריך את this.

  6. פונקציית setup מחזירה את אותו אוביקט "דבק" שבגירסה הקודמת ויו יצר בשבילנו שכולל את הממשק לגשת לכל המידע המעניין בקומפוננטה. הפעם אנחנו מחליטים מה ייכנס או לא ייכנס לאוביקט זה. אוביקט הדבק שאנחנו מחזירים הוא מה שקוד הטמפלייט יכול לגשת אליו והוא שמחבר בין קוד הטמפלייט לקוד הסקריפט.

3. גירסה 3 - setup script

המבנה של Composition API מאפשר יותר שליטה על הקומפוננטה בהשוואה לכתיב הקלאסי. אם נרצה לקחת חלק מסוים בהתנהגות ולשתף אותו בין קומפוננטות כל מה שצריך לעשות הוא להוציא את החלק הזה לפונקציה נפרדת. לדוגמה אם הייתי רוצה לקחת את הרעיון של שעון שמוריד 1 מאיזשהו ערך הייתי יכול לכתוב פונקציה כזאת:

function withCountdown(start) {
    const ticks = ref(start);
    let timer;

    onMounted(() => {
      timer = setInterval(() => {
        ticks.value -= 1;
      }, 1000);
    });

    onUnmounted(() => {
      clearInterval(timer);
    });

    return ticks;
}

ואז קומפוננטה היתה יכולה להשתמש בשעון באופן הבא:

export default {
    setup() {
        const ticks = withCountdown(100);
        return { ticks };
    }
};

שיתוף קוד בין קומפוננטות הופך הרבה יותר קל כשיש לנו Composition API. אבל הצד השני של המטבע הוא שהכתיב באמצעות אוביקט הוא קצת מסורבל.

השלב הבא באבולוציה הוא להוסיף מנגנון לקומפיילר של ויו כדי שאני אכתוב רק את החלקים המעניינים והוא ישלים כבר לבד את כל השאר. הקוד בגירסה השלישית נראה כך:

<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue';

const props = defineProps({
  start: Number,
  step: Number,
});

const data = reactive({ value: props.start });
function inc() {
  console.log(data);
  data.value += props.step;
}

let timer;

onMounted(() => {
  timer = setInterval(() => {
    data.value -= 1;
  }, 1000);
});

onUnmounted(() => {
  clearInterval(timer);
});

</script>

<template>
  <div>
    <button @click="inc">{{data.value}}</button>
  </div>
</template>

פה כבר רואים חיסכון במספר שורות הקוד בתוכנית ולדעתי זו הגירסה הכי נקיה:

  1. הקוד כולו שאני כותב באופן אוטומטי יוכנס לתוך פונקציית setup.

  2. המקרו defineProps מחליף את המפתח props ואת העברת המשתנה props לפונקציה שהוגדרה ב setup.

  3. אין צורך להגדיר אוביקט ולהחזיר אותו, זה יקרה אוטומטית.

  4. אין צורך לבחור איזה משתנים ישמרו בתור שדות של אותו אוביקט דבק. הקומפיילר יבחר את כל המשתנים שהגדרתי ב Scope החיצוני ביותר של הסקריפט ויבנה את האוביקט מהם.

מבחינת יכולת ופיצ'רים קוד כזה שקול לקוד מהגירסה השניה, אבל מבחינת תחביר החיסכון ב Boilerplate אומר שאני מקבל קוד יותר קצר שיותר נעים לכתוב, לקרוא ולתחזק.

בשביל שיהיה קל לשחק עם הקוד העליתי את שלושת הגירסאות של הקומפוננטה למאגר גיט. מוזמנים לשכפל, להריץ ולבעוט בו כמה שתרצו: https://github.com/ynonp/vue-component-evolution.