Hướng dẫn học Vue 3 từ cơ bản đến nâng cao

Cơ bản về cú pháp

Tạo ứng dụng Vue:

Khởi tạo ứng dụng:

Mỗi ứng dụng Vue đều được tạo ra bằng hàm createApp để hình thành một instance ứng dụng.

import { createApp } from 'vue'

const appInstance = createApp({
  /* Cấu hình component gốc */
})
Component gốc:

Object truyền vào createApp thực chất là một component, mỗi ứng dụng cần có một "component gốc", các component khác sẽ đóng vai trò con của nó.

Nếu sử dụng single-file component, ta có thể import trực tiếp component gốc từ file khác.

import { createApp } from 'vue'
// Import component gốc từ file đơn
import RootComponent from './RootComponent.vue'

const appInstance = createApp(RootComponent)
Mount ứng dụng:

Instance ứng dụng phải gọi phương thức .mount() mới hiển thị được. Phương thức này nhận tham số "container", có thể là DOM element thật hoặc chuỗi CSS selector.

Cấu hình ứng dụng:

Instance ứng dụng cung cấp đối tượng .config cho phép cấu hình các tùy chọn toàn ứng dụng, ví dụ như thiết lập bộ xử lý lỗi toàn cục để bắt lỗi từ tất cả các component con:

appInstance.config.errorHandler = (error) => {
  /* Xử lý lỗi */
}
Nhiều instance ứng dụng:

[Hiếm dùng]

Cú pháp mẫu (template):

Hiển thị văn bản:
HTML nguyên bản:

Dấu ngoặc kép sẽ giải thích dữ liệu như văn bản thuần, không phải HTML. Nếu muốn chèn HTML, bạn cần dùng directive v-html:

Liên kết thuộc tính (Attribute binding):
  • Rút gọn tên giống nhau: Nếu tên attribute và giá trị JavaScript liên kết có cùng tên, có thể rút gọn cú pháp bằng cách bỏ qua giá trị attribute:
<!-- Tương đương với :id="id" -->
<div :id></div>

<!-- Cách này cũng hiệu lực -->
<div v-bind:id></div>
  • Liên kết nhiều giá trị động: Nếu bạn có một object JavaScript chứa nhiều attribute như sau:
const attributesObject = {
  id: 'container',
  class: 'wrapper',
  style: 'background-color:blue'
}

Sử dụng v-bind không có tham số, bạn có thể liên kết chúng vào một element duy nhất:

<div v-bind="attributesObject"></div>
Sử dụng biểu thức JavaScript:
  • Chỉ hỗ trợ biểu thức
  • Gọi hàm
  • Truy cập toàn cục bị giới hạn: Biểu thức trong mẫu sẽ bị sandbox hóa, chỉ truy cập được danh sách giới hạn các object toàn cục. Danh sách này bao gồm các object toàn cục phổ biến như MathDate. Các object toàn cục không nằm trong danh sách sẽ không thể truy cập trong biểu thức mẫu, ví dụ như thuộc tính người dùng gắn vào window. Tuy nhiên, bạn có thể thêm chúng rõ ràng vào app.config.globalProperties để sử dụng trong tất cả biểu thức Vue.
Directive
  • Tham số: Một số directive yêu cầu "tham số", được xác định bằng dấu hai chấm sau tên directive. Ví dụ: v-bind:href="url", href phía sau dấu hai chấm là tham số của directive v-bind.
<a v-bind:href="url"> ... </a>
  • Tham số động: Cũng có thể sử dụng biểu thức JavaScript làm tham số directive, cần đặt trong cặp dấu ngoặc vuông:
<a v-bind:[attributeName]="url"> ... </a>

Lưu ý: Giá trị tham số động trong directive có giới hạn, giá trị nên là chuỗi. Cú pháp cũng có một số hạn chế.

  • Modifier: Modifier là hậu tố đặc biệt bắt đầu bằng dấu chấm, cho biết directive cần được ràng buộc theo cách đặc biệt. Ví dụ modifier .prevent hướng dẫn directive v-on gọi event.preventDefault() khi sự kiện kích hoạt:
<form @submit.prevent="onSubmit">...</form>

Cơ sở phản ứng (Reactivity): [Nâng cao phản ứng]

ref():

Hàm ref(), trả về một object phản ứng kiểu RefImpl. Giá trị tham số được bao quanh trong thuộc tính value.

const counter = ref(0) // RefImpl { value: 0 }
  • Phản ứng sâu:
    • Giá trị không nguyên thủy sẽ được chuyển đổi thành proxy phản ứng thông qua reactive(), hàm này sẽ được thảo luận sau.
    • Cũng có thể sử dụng shallow ref để từ bỏ phản ứng sâu. Với shallow ref, chỉ việc truy cập .value mới được theo dõi. Shallow ref có thể dùng để tối ưu hiệu suất bằng cách tránh chi phí phản ứng cho dữ liệu lớn.
reactive():
  • Gán kiểu cho reactive()
  • <script setup>: Giống ref, tự động phơi bày các biến cấp cao.
Thời điểm cập nhật DOM:

Khi bạn sửa đổi trạng thái phản ứng, DOM sẽ tự động cập nhật. Tuy nhiên cần lưu ý rằng việc cập nhật DOM không đồng bộ. Vue sẽ đệm tất cả sửa đổi trạng thái trong chu kỳ "tick tiếp theo" để đảm bảo dù bạn thực hiện bao nhiêu lần sửa đổi trạng thái, mỗi component chỉ cập nhật một lần.

import { nextTick } from 'vue'

async function increase() {
  counter.value++
  await nextTick()
  // Bây giờ DOM đã cập nhật
}

Computed properties:

<script setup>
import { reactive, computed } from 'vue'

const authorInfo = reactive({
  name: 'Jane Smith',
  books: [
    'Vue 3 - Advanced Guide',
  ]
})

// Một computed property ref
const bookStatusMessage = computed(() => {
  return authorInfo.books.length > 0 ? 'Available' : 'Empty'
})
</script>
Gán kiểu cho computed properties
Computed properties có thể ghi

Biến được tạo bởi computed properties mặc định là chỉ đọc. Khi bạn cố gắng sửa đổi một computed property, bạn sẽ nhận cảnh báo runtime. Chỉ trong một số trường hợp đặc biệt bạn mới cần thuộc tính "có thể ghi".

<script setup>
import { ref, computed } from 'vue'

const first = ref('Jane')
const last = ref('Smith')

const full = computed({
  // getter
  get() {
    return first.value + ' ' + last.value
  },
  // setter
  set(newFull) {
    // Lưu ý: Chúng tôi đang sử dụng cú pháp destructuring ở đây
    [first.value, last.value] = newFull.split(' ')
  }
})
</script>

Lưu ý: Hàm set không nhất thiết phải thiết lập giá trị hiện tại, mà có thể làm việc khác. Ví dụ như component có ràng buộc v-model, và tương tác với component cha. Khi giá trị thay đổi, cần kích hoạt sự kiện tùy chỉnh của component cha, lúc này được kích hoạt trong set.

// Theo dõi sự kiện đóng của el-dialog, đồng bộ trạng thái với component cha
const dialogVisible = computed({
  get: () => props.modelValue,
  set: (value) => emit('update:modelValue', value)
});
Truy xuất giá trị trước:

Nếu cần, bạn có thể lấy giá trị trước đó của computed property bằng cách truy cập tham số đầu tiên của getter.

Watchers:

<script setup>
import { ref, watch } from 'vue'

const query = ref('')

// Có thể trực tiếp quan sát một ref
watch(query, async (newQuery, oldQuery) => {

})
</script>
Các loại nguồn dữ liệu cho watcher:

Tham số đầu tiên của watch có thể là các dạng "nguồn dữ liệu": có thể là một ref (bao gồm computed properties), một object phản ứng, một hàm getter, hoặc mảng các nguồn dữ liệu.

Deep watchers:

Truyền trực tiếp một object phản ứng vào watch(), sẽ tự động tạo deep watcher - hàm callback sẽ được kích hoạt khi bất kỳ thay đổi lồng nhau nào xảy ra:

const object = reactive({ count: 0 })

watch(object, (newValue, oldValue) => {
  // Được kích hoạt khi thuộc tính lồng nhau thay đổi
})

object.count++
Immediate watchers:

watch mặc định thực thi lười biếng: chỉ khi nguồn dữ liệu thay đổi, callback mới được thực thi. Nhưng trong một số trường hợp, chúng ta muốn ngay lập tức thực thi callback khi tạo watcher.

watch(
  source,
  (newValue, oldValue) => {
    // Thực thi ngay lập tức, và lại thực thi khi `source` thay đổi
  },
  { immediate: true } // Thiết lập gọi ngay lập tức
)
One-time watchers:

Mỗi khi nguồn được quan sát thay đổi, callback của watcher sẽ thực thi. Nếu muốn callback chỉ kích hoạt một lần khi nguồn thay đổi, hãy sử dụng tùy chọn once: true.

watch(
  source,
  (newValue, oldValue) => {
    // Khi `source` thay đổi, chỉ kích hoạt một lần
  },
  { once: true }
)
watchEffect():

watchEffect() cho phép chúng ta tự động theo dõi các phụ thuộc phản ứng của callback.

Nghĩa là, watchEffect là phiên bản rút gọn của watch. watchEffect không cần viết rõ ràng các nguồn phản ứng phụ thuộc, tự động lấy các nguồn phản ứng được sử dụng trong callback làm nguồn phụ thuộc. Mặc định là thực thi ngay lập tức, nên không cần chỉ định immediate: true.

watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${taskId.value}`
  )
  result.value = await response.json()
})
Dọn dẹp side effect:
Thời điểm kích hoạt callback:
  • Post Watchers
  • Synchronous watchers:
Dừng watcher:

Trong setup() hoặc <script setup>, các watcher được tạo bằng câu lệnh đồng bộ sẽ tự động ràng buộc với instance component chủ và sẽ tự động dừng khi component chủ bị hủy. Vì vậy, trong hầu hết các trường hợp, bạn không cần lo lắng về việc dừng một watcher.

Một điểm quan trọng là watcher phải được tạo bằng câu lệnh đồng bộ: nếu tạo watcher bằng callback bất đồng bộ, nó sẽ không ràng buộc với component hiện tại, bạn phải tự tay dừng nó để tránh rò rỉ bộ nhớ. Như ví dụ dưới đây:

<script setup>
import { watchEffect } from 'vue'

// Nó sẽ tự động dừng
watchEffect(() => {})

// ...cái này thì không!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

Template refs:

<input ref="input">
Truy cập template refs:

Để lấy ref trong Composition API, chúng ta có thể sử dụng hàm hỗ trợ useTemplateRef()

<script setup>
import { useTemplateRef, onMounted } from 'vue'

// Tham số đầu tiên phải khớp với giá trị ref trong template
const input = useTemplateRef('my-input')

onMounted(() => {
  input.value.focus()
})
</script>

<template>
  <input ref="my-input" />
</template>

Trước phiên bản vue3.5 không có hàm useTemplateRef. Nên trước 3.5, cách dùng là khai báo một ref trùng với tên attribute ref trong template:

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

// Khai báo một ref để chứa tham chiếu đến element
// Phải trùng tên với ref trong template
const input = ref(null)

onMounted(() => {
  input.value.focus()
})
</script>

<template>
  <input ref="input" />
</template>
Template refs trong v-for

Khi sử dụng template refs trong v-for, ref tương ứng chứa giá trị là mảng, sẽ chứa tất cả các phần tử trong danh sách sau khi mount:

<script setup>
import { ref, useTemplateRef, onMounted } from 'vue'

const items = ref([
  /* ... */
])

const itemRefs = useTemplateRef('items')

onMounted(() => console.log(itemRefs.value))
</script>

<template>
  <ul>
    <li v-for="item in items" ref="items">
      {{ item }}
    </li>
  </ul>
</template>
Function template refs
<input :ref="(el) => { /* Gán el cho thuộc tính dữ liệu hoặc biến ref */ }">
  • ref còn có thể ràng buộc với hàm, sẽ được gọi mỗi khi component cập nhật. Hàm này nhận element ref làm tham số đầu tiên.
  • Khi element được ràng buộc bị hủy, hàm cũng được gọi một lần nữa, lúc này tham số el sẽ là null. Tất nhiên bạn cũng có thể ràng buộc một phương thức component thay vì hàm nội tuyến.
Ref trên component:

Template refs cũng có thể được sử dụng trên component con. Trong trường hợp này, giá trị nhận được trong ref là instance của component

Cơ bản về component

Định nghĩa component:

Khi sử dụng bước xây dựng, chúng ta thường định nghĩa component Vue trong file riêng biệt có đuôi .vue, gọi là single-file component (viết tắt là SFC).

Sử dụng component:
  • Đăng ký toàn cục
  • Đăng ký cục bộ

Lưu ý:

1. Trong single-file component, khuyến nghị sử dụng tên tag PascalCase cho component con để phân biệt với element HTML nguyên thủy. Mặc dù tên tag HTML gốc không phân biệt hoa thường, nhưng Vue single-file component có thể phân biệt hoa thường trong quá trình biên dịch. Chúng ta cũng có thể sử dụng /> để đóng tag.

2. Nếu bạn trực tiếp viết template trong DOM (ví dụ nội dung element <template> nguyên thủy), việc biên dịch template phải tuân thủ hành vi phân tích HTML trong trình duyệt. Trong trường hợp này, bạn nên sử dụng dạng kebab-case và đóng tag rõ ràng cho các component này. (tức là viết template trực tiếp trong file html)

Truyền props
<script setup>
const props = defineProps(['title'])
console.log(props.title)
</script>

<template>
  <h4>{{ title }}</h4> <!-- Ở đây dùng trực tiếp giá trị trong props, không cần prefix props -->
</template>
  • defineProps khai báo props sẽ tự động phơi bày cho template. Và defineProps sẽ trả về object chứa tất cả props có thể truyền cho component. Nghĩa là, khi dùng props trong template, không cần thêm tiền tố props, dùng trực tiếp thuộc tính bên trong là được.
Theo dõi sự kiện
<script setup>
const emit = defineEmits(['enlarge-text'])

emit('enlarge-text')
</script>

Sử dụng $emit trực tiếp trong template

<button @click="$emit('increaseBy')">Increase</button>

Lưu ý: Dù khai báo rõ ràng sự kiện tùy chỉnh không bắt buộc, nhưng rất khuyến khích khai báo. Nhiều quy tắc eslint được tạo ra yêu cầu điều này.

Đặc điểm: Nếu sử dụng khai báo rõ ràng, Vue sẽ biết đây là sự kiện tùy chỉnh, không cố gắng xử lý như sự kiện DOM nguyên thủy.

Phân phối nội dung qua slots

Sử dụng <slot> như placeholder, nội dung được truyền từ component cha sẽ render tại đây.

Dynamic component <component>

Một số trường hợp cần chuyển đổi giữa hai component, ví dụ giao diện Tab:

<!-- component thay đổi khi currentTab thay đổi -->
<component :is="tabs[currentTab]"></component>

Trong ví dụ trên, giá trị được truyền cho :is có thể là:

  • Tên component đã đăng ký
  • Object component đã import

Chiêm nghiệm: Đối với các component chuyển đổi luân phiên này, sử dụng dynamic component tốt hơn so với dùng v-if.

Lưu ý về phân tích template trong DOM:

Nghĩa là tạo template DOM trong file html, không phải trong file .vue đơn.

Life cycle

Hiểu sâu hơn về component

2.1 Đăng ký component

Đăng ký toàn cục:

Chúng ta có thể sử dụng phương thức .component() của instance Vue application để làm component khả dụng toàn cục trong ứng dụng Vue hiện tại.

app.component(
  // Tên đăng ký
  'MyComponent',
  // Triển khai component
  {
    /* ... */
  }
)

Nếu là single-file component, có thể đăng ký file .vue đã import:

import MyComponent from './App.vue'

app.component('MyComponent', MyComponent)

Phương thức .component() có thể được nối chuỗi:

app
  .component('ComponentA', ComponentA)
  .component('ComponentB', ComponentB)
  .component('ComponentC', ComponentC)
Đăng ký cục bộ:

Trong single-file component dùng <script setup>, component đã import có thể sử dụng trực tiếp trong template mà không cần đăng ký:

<script setup>
import ComponentA from './ComponentA.vue'
</script>

<template>
  <ComponentA />
</template>

Nếu không dùng <script setup>, cần sử dụng tùy chọn components để đăng ký rõ ràng:

import ComponentA from './ComponentA.js'

export default {
  components: {
    ComponentA
  },
  setup() {
    // ...
  }
}
Định dạng tên component: PascalCase và kebab-case

Việc import chắc chắn dùng định dạng PascalCase, việc sử dụng có thể chọn một trong hai định dạng trên.

  • PascalCase: Khuyến nghị cách này. Lý do là dùng kebab-case gặp phải một lỗi.
  • kebab-case: Vue hỗ trợ phân tích tag kebab-case trong template thành component đăng ký theo định dạng PascalCase. Vì vậy component được import theo định dạng PascalCase có thể dùng tag kebab-case để biểu diễn.

Kinh nghiệm gặp lỗi: Trong component dùng script-setup import component con UpdateLog, dùng tag định dạng kebab-case, kết quả component con không load được. Kiểm tra kỹ không thấy lỗi chính tả, đổi tag thành định dạng PascalCase thì ổn. Sau đó phát hiện ra, trong setup còn khai báo thêm hàm updateLog. Nghĩa là:

<update-log></update-log>
<script lang="ts" setup>
import { defineComponent, h, reactive, ref } from 'vue';
import UpdateLog from "./UpdateLog.vue";

const updateLog = () => {}
</script>

Phân tích vấn đề: Về mặt JS, UpdateLog và updateLog là hai biến tên khác nhau. Nhưng tên tag kebab-case trong template được phân tích thành camelCase (trùng cả camelCase lớn/nhỏ), phân tích xong trùng với tên hàm updateLog. Nên không tìm thấy biến component đúng; còn PascalCase không như vậy, vì single-file component có thể phân biệt hoa thường trong quá trình biên dịch, không cần phân tích thành biến khác để khớp component, mà tìm trực tiếp biến cùng tên.

Thực tế kiểm tra: Tag kebab-case, tên component import, dùng định dạng camelCase lớn/nhỏ, đều khớp đúng.

Kinh nghiệm: Trong single-file component, thống nhất dùng tag component định dạng PascalCase, có thể tránh vấn đề nêu trên. Hơn nữa định dạng PascalCase dễ phân biệt với element HTML nguyên thủy.

Nhiều thư viện component dùng ví dụ định dạng kebab-case, có thể vì cân nhắc việc viết template trên DOM.

2.2 Props

Khởi tạo Props:

Trong single-file component dùng <script setup>, props có thể được khai báo bằng macro defineProps():

<script setup>
const props = defineProps(['foo'])

console.log(props.foo)
</script>

Trong component không dùng <script setup>, props có thể được khai báo bằng tùy chọn props:

export default {
  props: ['foo'],
  setup(props) {
    // setup() nhận props làm tham số đầu tiên
    console.log(props.foo)
  }
}

Lưu ý tham số truyền cho defineProps() và giá trị cung cấp cho tùy chọn props là giống nhau, hai cách khai báo thực chất đều dùng tùy chọn props.

Phân rã Props phản ứng:
const { foo } = defineProps(['foo'])

watchEffect(() => {
  // Trước 3.5 chỉ chạy một lần
  // Trong 3.5+ khi prop "foo" thay đổi sẽ chạy lại
  console.log(foo)
})

Có thể sử dụng cú pháp giá trị mặc định native của JavaScript để khai báo giá trị mặc định của props. Điều này đặc biệt hữu ích khi dùng khai báo props dựa trên kiểu dữ liệu.

const { foo = 'hello' } = defineProps<{ foo?: string }>()

Khó hiểu: Phần này hơi khó hiểu, lần tới bổ sung thêm

Chi tiết truyền prop
Dòng dữ liệu một chiều
Xác thực Prop

Khi giá trị không đáp ứng yêu cầu kiểu, Vue sẽ đưa ra cảnh báo trong console để nhắc nhở người dùng. Điều này rất hữu ích khi phát triển component cho các nhà phát triển khác sử dụng.

Để khai báo xác thực props, bạn có thể cung cấp object chứa tùy chọn xác thực props cho macro defineProps():

defineProps({
  // ------------------  Kiểm tra kiểu cơ bản  -----------------------------
  // (giá trị `null` và `undefined` sẽ bỏ qua mọi kiểm tra kiểu)
  propA: Number,
  // Nhiều kiểu có thể xảy ra
  propB: [String, Number],
  // Bắt buộc, và kiểu String
  propC: {
    type: String,
    required: true
  },
  // Chuỗi bắt buộc nhưng có thể là null
  propD: {
    type: [String, null],
    required: true
  },
  // Giá trị mặc định kiểu Number
  propE: {
    type: Number,
    default: 100
  },
  // ------------------  Giá trị mặc định kiểu object  --------------------
  propF: {
    type: Object,
    // Giá trị mặc định object hoặc array
    // phải trả về từ hàm factory.
    // Hàm này nhận prop gốc component nhận được làm tham số.
    default(rawProps) {
      return { message: 'hello' }
    }
  },
  // Hàm xác thực kiểu tùy chỉnh
  // Trong 3.4+, props đầy đủ được truyền vào làm tham số thứ hai
  propG: {
    validator(value, props) {
      // Giá trị phải trùng một trong các chuỗi này
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // Giá trị mặc định kiểu Function
  propH: {
    type: Function,
    // Không giống giá trị mặc định object hoặc array, đây không phải
    // hàm factory. Đây sẽ là hàm được dùng làm giá trị mặc định
    default() {
      return 'Default function'
    }
  }
})
Chuyển đổi kiểu Boolean

2.3 Sự kiện

Kích hoạt và theo dõi sự kiện: giống vue2
<button @click="$emit('someEvent')">Click Me</button>
Tham số sự kiện: giống vue2
Khởi tạo sự kiện kích hoạt:

Chúng ta không thể dùng phương thức $emit trong <template> trong phần <script setup> của component, nhưng defineEmits() sẽ trả về hàm cùng chức năng để chúng ta sử dụng:

<script setup>
const emit = defineEmits(['inFocus', 'submit'])

function buttonClick() {
  emit('submit')
}
</script>

Nếu dùng hàm setup rõ ràng thay cho <script setup>, sự kiện cần được định nghĩa qua tùy chọn emits, hàm emit cũng được phơi bày trên object ngữ cảnh của setup():

export default {
  emits: ['inFocus', 'submit'],
  setup(props, ctx) {
    ctx.emit('submit')
  }
}

Giải thích: $emit có thể dùng trực tiếp trong template, nhưng trong setup không thể dùng trực tiếp, cần khai báo trước.

Lưu ý: Nếu tên sự kiện nguyên thủy (ví dụ click) được định nghĩa trong tùy chọn emits, listener sẽ chỉ lắng nghe sự kiện click do component kích hoạt mà không phản hồi sự kiện click nguyên thủy.

Nghĩa là, component con rõ ràng khai báo sự kiện kích hoạt, thì sự kiện tương ứng chắc chắn là sự kiện tùy chỉnh của component. Không coi là sự kiện nguyên thủy của root element.

Xác thực sự kiện:

Giống cách thêm xác thực kiểu cho props, tất cả sự kiện kích hoạt cũng có thể được mô tả bằng dạng object.

vue3 không có modifier .native: Làm thế nào để ràng buộc sự kiện nguyên thủy lên root element của component.
  • Component đơn root: Mặc định là ràng buộc lên root element, nên không cần modifier .native. Sự kiện nguyên thủy của component sẽ tự động truyền qua root element. [Xem mục "Transmitted Attributes"]
  • Component đa root: Component đa root không có hành vi truyền tự động attribute. Vue không biết truyền đi đâu, còn đưa ra cảnh báo.

2.4 Component v-model

v-model có thể được sử dụng trên component để thực hiện ràng buộc hai chiều.

Cách dùng cơ bản:
  • Cơ chế bên dưới:
    • Bộ biên dịch sẽ biên dịch v-model thành prop modelValue;
    • v-model="foo" trong component cha sẽ được biên dịch thành tag có sự kiện tùy chỉnh:
<Child v-model="foo" />

Biên dịch sau:

<Child
  :modelValue="foo"
  @update:modelValue="$event => (foo = $event)"
>
</Child>

Vì vậy component cha không cần lắng nghe sự kiện cho ràng buộc dữ liệu này. Bên dưới đã làm rồi.

Component con phải nhận prop modelValue này, và thông qua emit (sự kiện @update:modelValue) kích hoạt cập nhật component cha.

  • Từ Vue 3.4 trở đi, cách thực hiện được khuyến nghị là dùng macro defineModel(): Cách này, ngay cả cách kích hoạt thủ công trong component con cũng không cần nữa, đã xử lý sẵn cho bạn.
<!-- Child.vue -->
<script setup>
const model = defineModel()

function update() {
  model.value++
}
</script>

<template>
  <div>Parent bound v-model is: {{ model }}</div>
  <button @click="update">Increment</button>
</template>

Component cha có thể ràng buộc giá trị với v-model:

<!-- Parent.vue -->
<Child v-model="countModel" />
Tham số của v-model:
Ràng buộc nhiều v-model:
Xử lý modifier v-model:

2.5 Transmitted Attributes

"Transmitted attribute" là attribute được truyền cho component nhưng không được khai báo là props hay emits hoặc listener sự kiện v-on. Ví dụ phổ biến nhất là class, styleid.

Kế thừa Attributes:

Khi component render với một element đơn làm root, transmitted attribute sẽ tự động được thêm vào root element.

  • Hợp nhất classstyle: Nếu root element của component con đã có attribute class hoặc style, nó sẽ hợp nhất với giá trị kế thừa từ component cha.
  • Kế thừa listener v-on: Quy tắc tương tự áp dụng cho listener sự kiện v-on.
<MyButton @click="onClick" />

Listener click sẽ được thêm vào root element của <MyButton>, tức là element <button> nguyên thủy. Khi <button> nguyên thủy được click, sẽ kích hoạt phương thức onClick của component cha. Tương tự, nếu element button nguyên thủy cũng ràng buộc listener sự kiện qua v-on, thì cả listener này và listener kế thừa từ component cha đều được kích hoạt.

Giải thích: Từ đây có thể thấy, ràng buộc sự kiện nguyên thủy cho component vue3 hoàn toàn không cần modifier .native nữa. Mặc định đã truyền qua root node rồi.

  • Kế thừa component sâu:

Trong một số trường hợp component render component khác tại root node.

Vô hiệu hóa kế thừa Attributes:

Nếu bạn không muốn component tự động kế thừa attribute, bạn có thể đặt inheritAttrs: false trong tùy chọn component.

<script setup>
defineOptions({
  inheritAttrs: false
})
// ...logic setup
</script>
Kế thừa Attributes cho nhiều root:

Khác với component đơn root, component có nhiều root node không có hành vi truyền attribute tự động. Nếu $attrs không được ràng buộc rõ ràng, sẽ đưa ra cảnh báo runtime.

Truy cập Transmitted Attributes trong JavaScript:

Nếu cần, bạn có thể sử dụng API useAttrs() trong <script setup> để truy cập tất cả transmitted attribute của component:

<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
</script>

Nếu không dùng <script setup>, attrs sẽ được phơi bày như thuộc tính của object ngữ cảnh setup():

export default {
  setup(props, ctx) {
    // Transmitted attribute được phơi bày như ctx.attrs
    console.log(ctx.attrs)
  }
}

2.6 Slots

Nội dung và cổng slot:

Nội dung slot có thể truy cập phạm vi dữ liệu component cha, vì nội dung slot được định nghĩa trong template component cha.

Phạm vi render:
  • Nội dung slot có thể truy cập phạm vi dữ liệu component cha, vì nội dung slot được định nghĩa trong template component cha.
  • Nội dung slot không thể truy cập dữ liệu component con. Trừ khi cung cấp dữ liệu qua scoped slot.
Nội dung mặc định:
Named slots:
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

Khi sử dụng, component cha

<BaseLayout>
  <template v-slot:header>
    <!-- Nội dung slot header đặt ở đây -->
  </template>
</BaseLayout>

v-slot tương ứng với viết tắt #, vì vậy có thể viết ngắn thành <template #header>

Conditional slots: Dựa vào việc có nội dung slot để quyết định render nội dung nào đó.

Kết hợp sử dụng thuộc tính $slots với v-if để thực hiện.

<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>
  </div>
</template>

Nội dung slot vốn là tùy chọn, vậy tại sao cần conditional slot? Thực hiện tình huống phức tạp sau:

  • Điều khiển render nội dung slot qua v-if, tránh node DOM không cần thiết. Như trên, không có slot, parent node của nội dung slot là không cần thiết, thực hiện qua conditional slot.
  • Không nhất thiết $slots quyết định tình huống render, qua props component cũng có thể quyết định slot nào render.
<!-- Component con Card.vue -->
<template>
  <div class="card">
    <header v-if="showHeader">
      <slot name="header"></slot> <!-- Chỉ render khi showHeader là true -->
    </header>
  </div>
</template>
<script>
export default {
  props: {
    showHeader: Boolean // Điều khiển có render header slot không
  }
};
</script>

<!-- Component cha Parent.vue -->
<template>
  <Card :showHeader="false"> <!-- Ngay cả khi component cha truyền nội dung header, cũng không render -->
    <template #header>This content won't be rendered.</template>
    Default content
  </Card>
</template>
Tên slot động: Khi sử dụng, v-slot chỉ định tên slot là động. [Ít dùng]
Scoped slots:

Trong phần phạm vi render nêu trên, chúng ta đã thảo luận rằng nội dung slot không thể truy cập trạng thái component con.

Tuy nhiên trong một số trường hợp nội dung slot có thể muốn sử dụng dữ liệu từ cả phạm vi component cha và component con. Để làm được điều này, chúng ta cần phương pháp để component con cung cấp một phần dữ liệu cho slot khi render.

  • Scoped slot mặc định:
<MyComponent v-slot="{ text, count }">
  {{ text }} {{ count }}
</MyComponent>
  • Named scoped slots:
<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

  <template #default="defaultProps">
    {{ defaultProps }}
  </template>

  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>
  • Nếu bạn sử dụng cả named slot và default slot, cần dùng thẻ <template> rõ ràng cho default slot.
<MyComponent>
  <!-- Sử dụng default slot rõ ràng -->
  <template #default="{ message }">
    <p>{{ message }}</p>
  </template>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</MyComponent>

Tổng kết:

  1. Scoped slot mặc định, lấy phạm vi tại tag component. Named scoped slot lấy phạm vi tại template có tên, như <template #header="headerProps">
  2. Cách component nội bộ cung cấp dữ liệu phạm vi là thêm thuộc tính vào thẻ slot, như dưới, cung cấp dữ liệu message cho slot header.
<!-- template <MyComponent> -->
<div>
  <slot name="header" message="hello"></slot>
</div>
  1. Khi sử dụng component, lấy scoped slot qua biến bên phải dấu bằng của v-slot.
<MyComponent>
  <template #header="headerProps">
    <p>{{headerProps.message}}</p>
  </template>
</MyComponent>

2.7 Dependency injection

Vấn đề truyền prop cấp tầng: Nếu chỉ dùng props thì phải truyền dần xuống theo chuỗi component, điều này rất phiền phức.
Provide:

Để cung cấp dữ liệu cho hậu duệ component, cần dùng hàm provide():

<script setup>
import { provide } from 'vue'

provide(/* tên inject */ 'message', /* giá trị */ 'hello!')
</script>
Provide cấp ứng dụng: tức là cung cấp từ instance app toàn cục
import { createApp } from 'vue'

const app = createApp({})

app.provide(/* tên inject */ 'message', /* giá trị */ 'hello!')
Inject:

Để inject dữ liệu do component cấp trên cung cấp, cần dùng hàm inject():

<script setup>
import { inject } from 'vue'

const message = inject('message')
</script>
Giá trị mặc định inject:

Nếu khi inject một giá trị không yêu cầu phải có người cung cấp, chúng ta nên khai báo giá trị mặc định, giống props

const value = inject('message', 'Đây là giá trị mặc định')
Sử dụng với dữ liệu phản ứng:

Khi cung cấp / inject dữ liệu phản ứng, nên giữ mọi thay đổi trạng thái phản ứng ở component cung cấp càng nhiều càng tốt. Điều này giúp đảm bảo khai báo và thay đổi trạng thái cung cấp đều tập trung trong cùng một component, khiến dễ bảo trì hơn.

Sử dụng Symbol làm tên inject:

Nếu bạn đang xây dựng ứng dụng lớn, chứa nhiều dependency được cung cấp, hoặc bạn đang viết thư viện component cho nhà phát triển khác sử dụng, tốt nhất nên dùng Symbol làm tên inject để tránh xung đột tiềm ẩn.

2.8 Async components

Logic reuse

3.1 Composable functions

"Composable functions" (Composables) là hàm sử dụng Composition API của Vue để đóng gói và tái sử dụng logic trạng thái.

Cũng còn gọi là hooks, còn tên "Hooks" nhiều hơn là cộng đồng và nhà phát triển mượn thói quen đặt tên React Hooks.

Ví dụ theo dõi chuột:
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// Theo quy ước, tên composable function bắt đầu bằng "use"
export function useMousePosition() {
  // Trạng thái được quản lý và đóng gói bởi composable function
  const xPosition = ref(0)
  const yPosition = ref(0)

  // Composable function có thể thay đổi trạng thái bất kỳ lúc nào.
  function handleMove(event) {
    xPosition.value = event.clientX
    yPosition.value = event.clientY
  }

  // Một composable function cũng có thể gắn vào lifecycle của component chủ
  // để khởi động và dỡ bỏ side effect
  onMounted(() => window.addEventListener('mousemove', handleMove))
  onUnmounted(() => window.removeEventListener('mousemove', handleMove))

  // Phơi bày trạng thái được quản lý qua giá trị trả về
  return { xPosition, yPosition }
}

Sử dụng trong component:

<script setup>
import { useMousePosition } from './mouse.js'

const { xPosition, yPosition } = useMousePosition()
</script>

<template>Mouse position is at: {{ xPosition }}, {{ yPosition }}</template>
Sử dụng composable functions trong Options API:

Nếu bạn đang dùng Options API, composable function phải được gọi trong setup(). Và ràng buộc trả về từ nó phải được trả về trong setup() để phơi bày cho this và template:

import { useMousePosition } from './mouse.js'
import { useFetch } from './fetch.js'

export default {
  setup() {
    const { xPosition, yPosition } = useMousePosition()
    const { data, error } = useFetch('...')
    return { xPosition, yPosition, data, error }
  },
  mounted() {
    // Thuộc tính được phơi bày trong setup() có thể truy cập qua `this`
    console.log(this.xPosition)
  }
  // ...các tùy chọn khác
}

3.2 Custom directives

3.3 Plugins

Giới thiệu:

Một plugin có thể là object có phương thức install(), hoặc trực tiếp là function cài đặt. Function cài đặt sẽ nhận instance ứng dụng cài nó và tùy chọn truyền cho app.use() làm tham số:

const myPlugin = {
  install(app, options) {
    // Cấu hình ứng dụng này
  }
}

Plugin phát huy tác dụng trong các trường hợp phổ biến sau:

  1. Đăng ký một hoặc nhiều component hoặc directive toàn cục qua app.component()app.directive().
  2. Làm tài nguyên có thể inject vào toàn ứng dụng qua app.provide().
  3. Thêm một số thuộc tính hoặc phương thức toàn cục vào app.config.globalProperties
  4. Một thư viện chức năng có thể bao gồm cả ba điều trên (ví dụ vue-router).
  5. Plugin đóng gói lại plugin đã có [chủ yếu thuận tiện quản lý code]

Tất cả sau, chúng ta cần đặt các dependency và cấu hình liên quan đến một plugin vào file riêng. main.js chỉ cần import một file cài đặt là được. Cách này thực hiện qua plugin cũng rất phổ biến.

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
/* Có thể xử lý các cấu hình khác của element ở đây */
export default {
    install: (app) => {
        app.use(ElementPlus)
    }
}

4 Built-in components

4.1 Transition:

Component <Transition>
<button @click="show = !show">Toggle</button>
<Transition>
  <p v-if="show">hello</p>
</Transition>
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}

Lưu ý: <Transition> chỉ hỗ trợ một element hoặc component làm nội dung slot. Nếu nội dung là component, component này phải có một root element duy nhất.

Hiệu ứng chuyển tiếp CSS
  • Class chuyển tiếp CSS
  • Đặt tên hiệu ứng chuyển tiếp: Chúng ta có thể truyền prop name cho component <Transition> để khai báo tên hiệu ứng chuyển tiếp.
<Transition name="fade">
  ...
</Transition>
  • CSS transition
  • CSS animation
  • Custom transition class
  • Sử dụng đồng thời transition và animation
  • Chuyển tiếp sâu và độ dài chuyển tiếp rõ ràng
JavaScript hooks
Hiệu ứng chuyển tiếp có thể tái sử dụng
Chuyển tiếp xuất hiện
Chuyển tiếp giữa element
Chế độ chuyển tiếp
Chuyển tiếp giữa component
Chuyển tiếp động
Chuyển tiếp sử dụng Key Attribute

4.2 KeepAlive:

Cách dùng cơ bản:
<!-- Component không hoạt động sẽ được cache! -->
<KeepAlive>
  <component :is="activeComponent" />
</KeepAlive>
Bao gồm / Loại trừ:

<KeepAlive> mặc định cache tất cả instance component bên trong, nhưng chúng ta có thể tùy chỉnh hành vi này qua thuộc tính includeexclude. Giá trị của hai prop này có thể là chuỗi phân cách bằng dấu phẩy tiếng Anh, biểu thức regex, hoặc mảng chứa hai loại trên:

<!-- chuỗi phân cách bằng dấu phẩy tiếng Anh -->
<KeepAlive include="a,b">
  <component :is="view" />
</KeepAlive>

<!-- biểu thức regex (phải dùng `v-bind`) -->
<KeepAlive :include="/a|b/">
  <component :is="view" />
</KeepAlive>

<!-- mảng (phải dùng `v-bind`) -->
<KeepAlive :include="['a', 'b']">
  <component :is="view" />
</KeepAlive>
Số lượng instance cache tối đa: Chúng ta có thể giới hạn số lượng tối đa component instance có thể cache bằng cách truyền prop max.
Life cycle của instance cache:
  • onActivated():
  • onDeactivated():

Lưu ý:

  • onActivated cũng được gọi khi component mount, và onDeactivated cũng được gọi khi component unmount.
  • Hai hook này không chỉ áp dụng cho component root được cache bởi <KeepAlive>, mà còn cho hậu duệ trong cây cache. Nghĩa là hậu duệ cũng có hai lifecycle này. Nhưng lưu ý, component con không cache trạng thái.

4.3 Teleport: Thường dùng để truyền vào thẻ body

<Teleport> là component built-in, có thể "teleport" một phần template trong component đến vị trí ngoài cấu trúc DOM của component đó.

<Teleport to="body">
  <div>Nội dung bên trong Teleport</div>
</Teleport>

Trường hợp sử dụng: Rất hữu ích cho component popup; còn một trường hợp là component trong component. Khi view cha ẩn, component con cũng bị ẩn bị động. Trường hợp này dùng Teleport rất tốt. (Nếu component cha hoàn toàn hủy, component con cũng nên hủy)

5 Scaling applications

5.1 Single-file components: Xem chương 6

Single-file component của Vue (tức file *.vue, tiếng Anh Single-File Component, viết tắt là SFC) là định dạng file đặc biệt, cho phép đóng gói template, logic và style của component Vue vào một file duy nhất.

Tại sao dùng single-file component

Nếu trường hợp của bạn chỉ cần thêm tương tác đơn giản cho HTML tĩnh, bạn có thể xem petite-vue, đó là tập con của Vue đã tối ưu trước khoảng 6KB.

Single-file component hoạt động như thế nào
  • Vue single-file component là định dạng file được quy định bởi framework, vì vậy phải được biên dịch bởi @vue/compiler-sfc thành JavaScript và CSS chuẩn, một SFC sau biên dịch là module JavaScript (ES) chuẩn.
  • Tag <style> trong SFC thường được inject vào thẻ <style> nguyên thủy trong môi trường phát triển để hỗ trợ hot reload, còn trong môi trường sản xuất chúng sẽ được trích xuất và hợp nhất thành file CSS riêng biệt.

5.2 Toolchain

Project scaffolding
IDE support:

IDE được khuyến nghị là VS Code, kết hợp extension Vue - Official (trước đây là Volar).

Browser developer plugins
typescript
Code linting
Formatting

5.3 Routing: Xem tài liệu khác

5.4 State management

5.5 Server-side rendering

Advanced topics

6.1 Various ways to use Vue

6.2 Render functions & JSX

Cách dùng cơ bản:
  • Tạo Vnodes: Vue cung cấp hàm h() để tạo vnodes.
  • Khởi tạo render function: Khi dùng Composition API với template, giá trị trả về của hook setup() dùng để phơi bày dữ liệu cho template. Tuy nhiên khi dùng render function, có thể trả trực tiếp render function:
import { ref, h } from 'vue'

export default {
  props: {
    /* ... */
  },
  setup(props) {
    const count = ref(1)

    // Trả về render function
    return () => h('div', props.msg + count.value)
  }
}

Lưu ý: Hãy đảm bảo trả về một function chứ không phải giá trị!

  • Vnodes phải duy nhất
JSX / TSX: [Scaffolding mặc định không hỗ trợ JSX, cần cài package phụ trợ]

JSX là phần mở rộng giống XML cho JavaScript, với nó, chúng ta có thể viết code theo cách sau:

const vnode = <div id={dynamicId}>hello, {userName}</div>

Lưu ý: Trong biểu thức JSX, dùng dấu ngoặc nhọn để nhúng giá trị động

Global API:---------------- Sau là tài liệu API

Composition API:

8.1 setup()

8.2 Reactive: Core

ref()
computed()
reactive()
readonly()
watchEffect()
watchPostEffect()
watchSyncEffect()
watch()
onWatcherCleanup()[^3.5+]

8.3 Reactive: Utilities

8.4 Reactive: Advanced

7) markRaw(): Đánh dấu object là không thể chuyển thành proxy. Trả về chính object đó.
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

// Cũng áp dụng cho nhúng trong object phản ứng khác, tức object phản ứng, thuộc tính bên trong nào đó không thể proxy (tức không phản ứng).
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false

Một object phản ứng, trong đó có một số giá trị thuộc tính, chúng ta không muốn nó phản ứng. Có thể dùng cái này để xử lý.

8.5 Lifecycle hooks

8.6 Dependency injection

8.7 Utilities

Built-in content:

Directives:

Single-file components:

11.1 Syntax definition

11.2 <script setup>

11.3 SFC CSS features

CSS scoped:
  • Root element component con:

Sau khi dùng scoped, style component cha sẽ không ảnh hưởng đến component con. Tuy nhiên, root node component con sẽ bị ảnh hưởng bởi cả style scoped component cha và component con.

  • Deep selectors: Selector trong style scoped nếu muốn chọn sâu hơn, tức: ảnh hưởng đến component con, có thể dùng :deep():
<style scoped>
.a :deep(.b) {
  /* ... */
}
</style>

:deep có thể không có selector phía trước:

<style scoped>
:deep(.b) {
  /* ... */
}
</style>

Biên dịch sau:

[data-v-f3f3eg9] .b {
  /* ... */
}

Từ đây có thể thấy, selector sau :deep vẫn giữ scope của component hiện tại, chỉ là có thể xuyên sâu vào bên trong. Phân tích底层: Sau khi dùng :deep, selector bên trong nó không thêm scope component nữa

  • Slot selectors: Theo mặc định, style scoped không ảnh hưởng đến nội dung render bởi <slot/>, vì chúng được coi là do component cha nắm giữ và truyền vào. Dùng pseudo-class :slotted để rõ ràng nhắm vào nội dung slot:
<style scoped>
:slotted(div) {
  color: red;
}
</style>
  • Global selectors: Nếu muốn một rule style áp dụng toàn cục, thay vì tạo <style> khác, có thể dùng pseudo-class :global để thực hiện (xem code bên dưới):
<style scoped>
:global(.red) {
  color: red;
}
</style>
  • Kết hợp style cục bộ và toàn cục:
<style>
/* Style toàn cục */
</style>

<style scoped>
/* Style cục bộ */
</style>

Phân tích thực tiễn: Sau khi dùng scoped, sẽ thêm attribute selector vào cuối selector. Nếu dùng deep selector :deep, sẽ thêm attribute selector cho phần trước nó.

.box .a {}
.a :deep(.b){}
:deep(.b){}

Biên dịch sau:

.box .a[data-v-f3f3eg9] {}
.a[data-v-f3f3eg9] .b {}
[data-v-f3f3eg9] .b {}
CSS Modules

Một tag <style module> sẽ được biên dịch thành CSS Modules và class CSS được tạo sẽ được phơi bày cho component như object $style. (tức biến css thành object js, dùng trong template như js)

v-bind() trong CSS

Single-file component, hỗ trợ dùng dữ liệu trạng thái component trong css, thông qua hàm v-bind.

<script setup>
import { ref } from 'vue'
const color = ref('red')
</script>

<style scoped>
p {
  color: v-bind('color'); /* Nếu là giá trị object, có thể dùng v-bind('theme.color') để lấy */
}
</style>

Advanced API

12.1 Custom elements:

12.2 Render functions:

h()

Tạo virtual DOM node (vnode)

  • Type:
// Chữ ký tham số đầy đủ
function h(
  type: string | Component,
  props?: object | null,
  children?: Children | Slot | Slots
): VNode

// Khi bỏ qua props, có thể đặt là null, hoặc không có mục này.
function h(type: string | Component, children?: Children | Slot): VNode
  • type: Tham số đầu tiên có thể là chuỗi (cho element nguyên thủy) hoặc định nghĩa component Vue.
  • prop: Tùy chọn. Tham số thứ hai là prop cần truyền
    • prop thông thường:
    • prop sự kiện: prop sự kiện, bắt đầu bằng onXxx tương đương với @xxx
// Truyền prop
h(Foo, {
  // Tương đương some-prop="hello"
  someProp: 'hello',
  // Tương đương @update="() => {}"
  onUpdate: () => {}
})
  • children: Tham số thứ ba là node con. Nếu có nhiều node con, children là mảng.
  • Thông tin chi tiết:
  • Tạo element nguyên thủy:
import { h } from 'vue'

// Ngoài type, các tham số khác đều tùy chọn
h('div')
h('div', { id: 'foo' })

// attribute và property đều có thể dùng làm prop
// Vue sẽ tự động chọn cách phân bổ đúng
h('div', { class: 'bar', innerHTML: 'hello' })

// class và style có thể viết dạng
// mảng hoặc object như trong template
h('div', { class: [foo, { bar }], style: { color: 'red' } })

// Listener sự kiện nên viết dạng onXxx
h('div', { onClick: () => {} })

// children có thể là chuỗi
h('div', { id: 'foo' }, 'hello')

// Có thể bỏ qua khi không có prop
h('div', 'hello')
h('div', [h('span', 'hello')])

// Mảng children có thể chứa cả vnode và chuỗi
h('div', ['hello', h('span', 'hello')])
  • Tạo component:
import Foo from './Foo.vue'

// Truyền prop
h(Foo, {
  // Tương đương some-prop="hello"
  someProp: 'hello',
  // Tương đương @update="() => {}"
  onUpdate: () => {}
})

// Truyền slot mặc định đơn
h(Foo, () => 'default slot')

// Truyền named slot
// Lưu ý, cần dùng `null` để tránh
// object slot bị coi là prop
h(MyComponent, null, {
  default: () => 'default slot',
  foo: () => h('div', 'foo'),
  bar: () => [h('span', 'one'), h('span', 'two')]
})

h() tạo component, truyền sự kiện tùy chỉnh, còn thấy một cách: đặt trong thuộc tính on:

h(Foo, {
  // Tương đương @update="() => {}"
  on{
    update: () => {}
  }
})

Thẻ: Vue3 JavaScript frontend web-development components

Đăng vào ngày 23 tháng 8 lúc 03:45