vuejs-dam-ba-pattern-pho-bien

Vuejs Design Pattern – Dăm ba pattern phổ biến

Làm việc với Vuejs applications là làm việc với Components, nhưng không phải là cứ component thì thảy tất cả vào.

Đối với những application lớn (large scale application), đòi hỏi người kĩ sử phải biết thêm về Vuejs Design Pattern để sử dụng tốt những pattern này

1. Cơ bản về Vuejs Design Pattern

Bắt đầu với câu hỏi Design Pattern là gì?

Hiểu sơ Vuejs Design Pattern có thể thông qua hai định nghĩa cơ bản sau:

A general reapeatable solution to a commonly occuring problem in software

Là giải pháp lặp đi lặp lại cho phần đề chung nhất trong software

Not tied to a specific problem. They’re very general and can be applied to a board spectrum of problems.

Không ràng buộc với một vấn đề cụ thể. Chúng rất chung chung và có thể được áp dụng cho một loạt các vấn đề.

Túm cái vậy lại thì đi sâu vào tìm hiểu từng pattern sẽ có ví dụ cụ thể. Nhưng pattern là mẫu chung nhất, nên hãy cố gắng suy nghĩ để hiểu concept của patten thì tốt hơn là ghi nhớ duy nhất về ví dụ

Nói về design pattern thì giới thiệu luôn cuốn Dive Into Design Patterns là đỉnh của chóp nha. Đọc xong thì master design pattern. Mua sách ở đây

2. Separation of Concerns

Mục đích của pattern này không gì ngoài

A principle for how applications should be architected

Là nguyên lý cho application nên được tổ chức như thế nào

Seperation là chia ra, theo cách hiểu này thì mỗi component sẽ được chia làm 2 phần (UI và Business). Concerns là mối quan tâm.

Trước khi bắt tay vào để hiểu tại sao cần chia ra như vậy. Cùng xem ví dụ dưới đây.

Để validation email, có 2 điều kiện ta cần phải check

  • Độ dài email phải lớn hơn hoặc bằng 4
  • Độ dài email lớn hơn 0

Đm, ví dụ đọc chán đời vcl, lớn hơn 4 kiểu gì chả lớn hơn 0. Nhưng tui chán nghĩ cái ví dụ cụ thể, nên các ông đọc tạm. Ok, lúc này ta cần bind class cho end user biết lúc nào email input đúng, khi nào sai

<template>
  <input
    type="email"
    class="form-control"
    placeholder="E-mail"
    :value="email"
    @input="update($event)"
    :class="{
      'is-valid': email.length >= 4 && email.length > 0,
      'is-invalid': !(email.length < 4 && email.length > 0),
    }"
  />
</template>

<script>
import validateEmail from '../validate-email';

export default {
  name: 'EmailInput',
  props: ['email'],
  methods: {
    update($event) {
      this.$emit('update:email', $event.target.value);
    },
    validateEmail,
  },
};
</script>

Trông cũng đâu tệ đúng không?. Nhưng tới hồi mà cái component nó lớn lên, tôi thề là các ông không phân biệt được đâu là UI, đâu là Business Logic code

Thay vì viết source logic loằng ngoằng trên teamplate (vốn là phần chỉ xử lý UI logic), ta có thể tách logic validation Email thành file js riêng

export default function validateEmail(email) {
  return email.length >= 4 && email.length > 0;
}

Phần xử lý logic cho validation email giờ đã nằm trong một file js duy nhất. Việc này tuân thủ Vuejs Design Pattern mà ta đã đề cập ở trên

Business Logic nằm ở một phần riêng biệt. Source code giờ chia thành 2 phần UI và Business, rất tốt cho reusable sau này!

Quay trở lại với EmailInput.vue

<template>
  <input
    type="email"
    class="form-control"
    placeholder="E-mail"
    :value="email"
    @input="update($event)"
    :class="{
      'is-valid': validateEmail(email),
      'is-invalid': !validateEmail(email),
    }"
  />
</template>

<script>
import validateEmail from '../validate-email';

export default {
  name: 'EmailInput',
  props: ['email'],
  methods: {
    update($event) {
      this.$emit('update:email', $event.target.value);
    },
    validateEmail,
  },
};
</script>

Phần binding class cho email này được sử dụng thông qua function validateEmail. Khá là tiện và tốt cho các ứng dụng Vue lớn.

3. Third-Party Libraries as Controlled Components

Trong quá trình development một Vuejs Applications, sẽ có lúc ta cần sử dụng các Libraries bên ngoài

Các libraries này gọi chung là Third-Party Libraries. Lấy một ví dụ đơn giản, trường hợp applications của các ông muốn cho phép end user gửi emoji. Thả phẫn nộ chẳng hạn, library các ông sẽ cân nhắc là emoji

https://emoji-button.js.org/

Với yêu cầu này, với một lối suy nghĩ thông thường và đơn giản. Ta có thể thảy library này thẳng vào App.vue

<template>
  <div id="app" class="row d-flex justify-content-center">
    <div class="col-md-5 mt-3">
      <div class="card">
        <div class="card-header">Emoji Selector</div>
        <div class="card-body">
          <form @submit.prevent="submit">
            <div class="form-group">
              <div class="input-group">
                <input type="text" class="form-control" readonly>
                <div class="input-group-append">
                  <button class="btn btn-outline-secondary" type="button">Button</button>
                </div>
              </div>
            </div>
            <button type="submit" class="btn btn-primary">Submit</button>
          </form>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      emoji: '',
    }
  },
  methods: {
    submit () {
      console.log(
        'Emoji: ',
        this.emoji
      );
    }
  }
}
</script>

<style src="bootstrap/dist/css/bootstrap.css">

Ban đầu thì vậy cũng ổn, nhưng tới khi applications sử dụng tới vài chục cái library vấn đề bắt đầu phát sinh. App.vue lúc này như một đống hỗn độn, lèo nhèo.

Chưa nói đến scalable, lúc này maintainable cũng đã là một vấn đề lớn.

Nếu sử dụng Vuejs Design Pattern, ta có thể bưng library vào trong component. Component bây giờ trở nên dễ dàng hơn để kiểm soát. Reusable khá ổn, nếu cần thì cứ import vào, như sau:

Tui tạo component EmojiInput.vue

<template>
  <div class="input-group">
    <input type="text" class="form-control" readonly :value="modelValue.emoji" />
    <div class="input-group-append">
      <button class="btn btn-outline-secondary" type="button" ref="emojiBtn">
        {{ modelValue ? modelValue.emoji : 'Select' }}
      </button>
    </div>
  </div>
</template>

<script>
import { EmojiButton } from '@joeattardi/emoji-button';

export default {
  name: 'EmojiInput',
  props: {
    modelValue: {
      required: true,
    },
    options: {
      default() {
        return {};
      },
    },
  },
  mounted() {
    const picker = new EmojiButton(this.options);
    const btn = this.$refs.emojiBtn;

    picker.on('emoji', (emoji) => {
      this.$emit('update:modelValue', emoji);
    });

    btn.addEventListener('click', () => {
      picker.togglePicker(btn);
    });
  },
};
</script>

Component này đứng độc lập, nhiệm vụ của nó chỉ là sử dụng Emoji từ Third-party library. Qua về các component khác hoặc App.vue, easy for us. Chỉ cần import vào là xài

<template>
  <div id="app" class="row d-flex justify-content-center">
    <div class="col-md-5 mt-3">
      <div class="card">
        <div class="card-header">Emoji Selector</div>
        <div class="card-body">
          <form @submit.prevent="submit">
            <div class="form-group">
              <EmojiInput v-model="emoji"
                :options="{ position: 'bottom' }" />
            </div>
            <button type="submit" class="btn btn-primary">Submit</button>
          </form>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import EmojiInput from '@/components/EmojiInput.vue';

export default {
  name: 'App',
  components: {
    EmojiInput,
  },
  data() {
    return {
      emoji: '',
    };
  },
  methods: {
    submit() {
      console.log('Emoji: ', this.emoji);
    },
  },
};
</script>

<style src="bootstrap/dist/css/bootstrap.css">
</style>
vuejs design pattern
Rồi xong, mọi thứ trở nên đơn giản. Dễ dùng, tốt cho maintain.

4. The Teleport Component

Bắt đầu với khái niệm chuẩn chỉnh nhất về Teleport ở trang chủ của Vuejs

Teleport provides a clean way to allow us to control under which parent in our DOM we want a piece of HTML to be rendered, without having to resort to global state or splitting this into two components.

Teleport cung cấp một cách hoàn hảo để control component cha nào muốn render thêm HTML ở trên DOM, nhưng không cần phải resort lại global state hoặc chia nó thành 2,3 component nhỏ.

Teleport ở đây cũng được hiểu như đường ống bên Mẽo hay ArabSaudi đã suy nghĩ và có plan để hiện thực hóa.

Thay vì chuyển người hay chuyển hàng thì ta chuyển code ra ngoài Vue application (ra ngoài <div id=”app”>)

Ví dụ cơ bản nhất là mở dialog, open modal

<body>
  <div style="position: relative;">
    <h3>Tooltips with Vue 3 Teleport</h3>
    <div>
      <modal-button></modal-button>
    </div>
  </div>
</body>

Để mang component show modal ra khỏi Vue, ta có thể sử dụng CreateElement, cách làm này hơi khoai vì source trong component không phải lúc nào cũng chỉ có vài dòng. Lớ ngớ làm loạn cả Vue Applications

const app = Vue.createApp({});

app.component('modal-button', {
  template: `
    <button @click="modalOpen = true">
        Open full screen modal!
    </button>

    <div v-if="modalOpen" class="modal">
      <div>
        I'm a modal! 
        <button @click="modalOpen = false">
          Close
        </button>
      </div>
    </div>
  `,
  data() {
    return {
      modalOpen: false
    }
  }
})

Với teleport, ta có thể đem source của modal ra ngoài Vue app mà không cần phải create element ở App.vue

    <teleport to="body">
      <div v-if="modalOpen" class="modal">
        <div>
          I'm a teleported modal! 
          (My parent is "body")
          <button @click="modalOpen = false">
            Close
          </button>
        </div>
      </div>
    </teleport>
vuejs design pattern

5. Tham khảo

Thank you for your time to read!. Have nice day

Có gì thắc mắc cứ comment đây nha! - Please feel free to comment here!