Chào mừng đến với Cafedev! Trong bài viết này, chúng ta sẽ khám phá về Vuejs với Render Functions & JSX. Đây là một phần của Vuejs mà không phải ai cũng biết đến, nhưng rất mạnh mẽ khi bạn cần sự linh hoạt và quyền lực lập trình của JavaScript. Hãy cùng tìm hiểu chi tiết về cách sử dụng Render Functions và JSX để xây dựng ứng dụng Vuejs của bạn một cách hiệu quả và linh hoạt hơn.
Vue khuyến khích sử dụng các mẫu để xây dựng ứng dụng trong hầu hết các trường hợp. Tuy nhiên, có những tình huống mà chúng ta cần sự mạnh mẽ của JavaScript. Đó là lúc chúng ta có thể sử dụng hàm render.
Nếu bạn mới làm quen với khái niệm về virtual DOM và hàm render, hãy đọc chương Cơ chế Render trước tiên.
Nội dung chính
1.Sử Dụng Cơ Bản
1.1 Tạo Vnodes
Vue cung cấp một hàm h()
để tạo ra các vnode:
import { h } from 'vue'
const vnode = h(
'div', // type
{ id: 'foo', class: 'bar' }, // props
[
/* children */
]
)
h()
là viết tắt của hyperscript – có nghĩa là “JavaScript tạo ra HTML (hypertext markup language)”. Tên này được thừa kế từ các quy ước được chia sẻ bởi nhiều triển khai virtual DOM. Một tên mô tả chi tiết hơn có thể là createVnode()
, nhưng một tên ngắn giúp khi bạn cần gọi hàm này nhiều lần trong một hàm render.
Hàm h()
được thiết kế để rất linh hoạt:
// all arguments except the type are optional
h('div')
h('div', { id: 'foo' })
// both attributes and properties can be used in props
// Vue automatically picks the right way to assign it
h('div', { class: 'bar', innerHTML: 'hello' })
// props modifiers such as `.prop` and `.attr` can be added
// with `.` and `^` prefixes respectively
h('div', { '.name': 'some-name', '^width': '100' })
// class and style have the same object / array
// value support that they have in templates
h('div', { class: [foo, { bar }], style: { color: 'red' } })
// event listeners should be passed as onXxx
h('div', { onClick: () => {} })
// children can be a string
h('div', { id: 'foo' }, 'hello')
// props can be omitted when there are no props
h('div', 'hello')
h('div', [h('span', 'hello')])
// children array can contain mixed vnodes and strings
h('div', ['hello', h('span', 'hello')])
Vnode kết quả có hình dạng sau:
const vnode = h('div', { id: 'foo' }, [])
vnode.type // 'div'
vnode.props // { id: 'foo' }
vnode.children // []
vnode.key // null
cảnh báo Lưu Ý Giao diện VNode
đầy đủ chứa nhiều thuộc tính nội bộ khác, nhưng rất khuyến khích tránh phụ thuộc vào bất kỳ thuộc tính nào ngoại trừ những thuộc tính được liệt kê ở đây. Điều này tránh việc gây ra sự cố không mong muốn nếu các thuộc tính nội bộ thay đổi.
1.2 Khai Báo Hàm Render
Khi sử dụng mẫu với Composition API, giá trị trả về của hook setup()
được sử dụng để tiết lộ dữ liệu cho mẫu. Tuy nhiên, khi sử dụng hàm render, chúng ta có thể trực tiếp trả về hàm render:
import { ref, h } from 'vue'
export default {
props: {
/* ... */
},
setup(props) {
const count = ref(1)
// return the render function
return () => h('div', props.msg + count.value)
}
}
Hàm render được khai báo bên trong setup()
nên tự nhiên nó có quyền truy cập vào props và bất kỳ trạng thái phản ứng nào được khai báo trong phạm vi tương tự.
Ngoài việc trả về một vnode duy nhất, bạn cũng có thể trả về chuỗi hoặc mảng:
export default {
setup() {
return () => 'hello world!'
}
}
import { h } from 'vue'
export default {
setup() {
// use an array to return multiple root nodes
return () => [
h('div'),
h('div'),
h('div')
]
}
}
tip Đảm bảo trả về một hàm thay vì trực tiếp trả về giá trị! Hàm setup()
chỉ được gọi một lần cho mỗi thành phần, trong khi hàm render trả về sẽ được gọi nhiều lần.
Còn dưới đây là cách code theo kiểu Options:
Chúng ta có thể khai báo hàm render bằng cách sử dụng tùy chọn render
:
import { h } from 'vue'
export default {
data() {
return {
msg: 'hello'
}
},
render() {
return h('div', this.msg)
}
}
Hàm render()
có quyền truy cập vào thể hiện của thành phần thông qua this
.
Ngoài việc trả về một vnode duy nhất, bạn cũng có thể trả về chuỗi hoặc mảng:
export default {
render() {
return 'hello world!'
}
}
import { h } from 'vue'
export default {
render() {
// use an array to return multiple root nodes
return [
h('div'),
h('div'),
h('div')
]
}
}
Nếu một thành phần hàm render không cần bất kỳ trạng thái thể hiện nào, chúng cũng có thể được khai báo trực tiếp như một hàm để ngắn gọn:
function Hello() {
return 'hello world!'
}
Đúng vậy, đây là một thành phần Vue hợp lệ! Xem 5. Các Thành Phần Hàm bên dưới để biết thêm chi tiết về cú pháp này.
1.3. Vnodes Phải là Độc Nhất
Tất cả các vnode trong cây thành phần phải là duy nhất. Điều đó có nghĩa là hàm render sau đây là không hợp lệ:
function render() {
const p = h('p', 'hi')
return h('div', [
// Yikes - duplicate vnodes!
p,
p
])
}
Nếu bạn thực sự muốn nhân bản cùng một phần tử/thành phần nhiều lần, bạn có thể làm điều đó bằng một hàm nhà máy. Ví dụ, hàm render sau đây là một cách hoàn toàn hợp lệ để vẽ 20 đoạn văn giống nhau:
function render() {
return h(
'div',
Array.from({ length: 20 }).map(() => {
return h('p', 'hi')
})
)
}
3. JSX / TSX
JSX là một phần mở rộng giống XML của JavaScript cho phép chúng ta viết mã như sau:
const vnode = <div>hello</div>
Trong biểu thức JSX, sử dụng dấu ngoặc nhọn để nhúng các giá trị động:
const vnode = <div id={dynamicId}>hello, {userName}</div>
create-vue
và Vue CLI đều có các tùy chọn để tạo các dự án với hỗ trợ JSX được cấu hình trước. Nếu bạn đang cấu hình JSX thủ công, vui lòng tham khảo tài liệu của @vue/babel-plugin-jsx để biết thêm chi tiết.
Mặc dù được giới thiệu lần đầu bởi React, JSX thực sự không có cú pháp thời gian chạy được xác định và có thể được biên dịch thành các đầu ra khác nhau. Nếu bạn đã làm việc với JSX trước đây, hãy lưu ý rằng Vue JSX transform khác với React’s JSX transform, vì vậy bạn không thể sử dụng JSX transform của React trong các ứng dụng Vue. Một số điểm khác biệt đáng chú ý so với JSX của React bao gồm:
- Bạn có thể sử dụng các thuộc tính HTML như
class
vàfor
như props – không cần sử dụngclassName
hoặchtmlFor
. - Truyền các phần tử con cho các thành phần (tức là slots) hoạt động khác nhau(Xem phần 4.6 Hiển thị Slots dưới đây).
Định nghĩa kiểu của Vue cũng cung cấp suy luận kiểu cho việc sử dụng TSX. Khi sử dụng TSX, hãy đảm bảo chỉ định “jsx”: “preserve” trong tsconfig.json để TypeScript giữ nguyên cú pháp JSX cho quá trình xử lý của Vue JSX transform.
3.1 Suy luận Kiểu JSX
Tương tự như việc biến đổi, JSX của Vue cũng cần các định nghĩa kiểu khác biệt.
Bắt đầu từ Vue 3.4, Vue không còn đăng ký toàn cục cho JSX
namespace nữa. Để chỉ dẫn TypeScript sử dụng các định nghĩa kiểu JSX của Vue, đảm bảo bao gồm các nội dung sau trong tsconfig.json
của bạn:
on
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "vue"
// ...
}
}
Bạn cũng có thể chọn lựa cho mỗi tệp bằng cách thêm một comment /* @jsxImportSource vue */
ở đầu tệp.
Nếu có mã nguồn phụ thuộc vào sự hiện diện của JSX
namespace toàn cục, bạn có thể giữ lại hành vi toàn cục trước Vue 3.4 bằng cách nhập hoặc tham chiếu một cách rõ ràng vue/jsx
trong dự án của bạn, điều này đăng ký JSX
namespace toàn cục.
4. Công thức Hàm Render
Dưới đây chúng tôi sẽ cung cấp một số công thức phổ biến để triển khai các tính năng mẫu dưới dạng các hàm render tương đương / JSX.
4.1 v-if
Mẫu:
<div>
<div v-if="ok">yes</div>
<span v-else>no</span>
</div>
Hàm render / JSX tương đương:
h('div', [ok.value ? h('div', 'yes') : h('span', 'no')])
<div>{ok.value ? <div>yes</div> : <span>no</span>}</div>
4.2 v-for
Mẫu:
<ul>
<li v-for="{ id, text } in items" :key="id">
{{ text }}
</li>
</ul>
Hàm render / JSX tương đương: Ví dụ theo 2 kiểu options và composition
Theo kiểu composition:
h(
'ul',
// assuming `items` is a ref with array value
items.value.map(({ id, text }) => {
return h('li', { key: id }, text)
})
)
<ul>
{items.value.map(({ id, text }) => {
return <li key={id}>{text}</li>
})}
</ul>
Theo kiểu Options:
h(
'ul',
this.items.map(({ id, text }) => {
return h('li', { key: id }, text)
})
)
<ul>
{this.items.map(({ id, text }) => {
return <li key={id}>{text}</li>
})}
</ul>
4.3v-on
Props với tên bắt đầu bằng on
theo sau là một chữ in hoa được xem như là trình nghe sự kiện. Ví dụ, onClick
tương đương với @click
trong mẫu.
h(
'button',
{
onClick(event) {
/* ... */
}
},
'click me'
)
<button
onClick={(event) => {
/* ... */
}}
click me
</button>
4.4 Bộ lọc Sự kiện
Đối với các bộ lọc sự kiện .passive
, .capture
, và .once
, chúng có thể được nối sau tên sự kiện bằng cách sử dụng camelCase.
Ví dụ:
h('input', {
onClickCapture() {
/* listener in capture mode */
},
onKeyupOnce() {
/* triggers only once */
},
onMouseoverOnceCapture() {
/* once + capture */
}
})
<input
onClickCapture={() => {}}
onKeyupOnce={() => {}}
onMouseoverOnceCapture={() => {}}
/>
Đối với các bộ lọc sự kiện và phím khác, withModifiers có thể được sử dụng:
import { withModifiers } from 'vue'
h('div', {
onClick: withModifiers(() => {}, ['self'])
})
x
<div onClick={withModifiers(() => {}, ['self'])} />
4.5 Các thành phần
Để tạo một vnode cho một thành phần, đối số đầu tiên được truyền vào h()
nên là định nghĩa của thành phần. Điều này có nghĩa là khi sử dụng hàm render, không cần phải đăng ký các thành phần – bạn có thể sử dụng các thành phần đã được nhập trực tiếp:
import Foo from './Foo.vue'
import Bar from './Bar.jsx'
function render() {
return h('div', [h(Foo), h(Bar)])
}
function render() {
return (
<div>
<Foo />
<Bar />
</div>
)
}
Như chúng ta có thể thấy, h
có thể hoạt động với các thành phần được nhập từ bất kỳ định dạng tệp nào miễn là nó là một thành phần Vue hợp lệ.
Các thành phần động làm việc một cách đơn giản với hàm render:
import Foo from './Foo.vue'
import Bar from './Bar.jsx'
function render() {
return ok.value ? h(Foo) : h(Bar)
}
x
function render() {
return ok.value ? <Foo /> : <Bar />
}
Nếu một thành phần được đăng ký bằng tên và không thể được nhập trực tiếp (ví dụ: đăng ký toàn cầu bởi một thư viện), nó có thể được giải quyết chương trình bằng cách sử dụng trợ giúp resolveComponent().
4.6 Hiển thị Slots
Trong hàm render, các slot có thể được truy cập từ ngữ cảnh setup()
. Mỗi slot trên đối tượng slots
là một hàm trả về một mảng của các vnode:
export default {
props: ['message'],
setup(props, { slots }) {
return () => [
// default slot:
// <div><slot /></div>
h('div', slots.default()),
// named slot:
// <div><slot name="footer" :text="message" /></div>
h(
'div',
slots.footer({
text: props.message
})
)
]
}
}
Tương đương với JSX:
// default
<div>{slots.default()}</div>
// named
<div>{slots.footer({ text: props.message })}</div>
Trong hàm render, các slot có thể được truy cập từ this.$slots:
export default {
props: ['message'],
render() {
return [
// <div><slot /></div>
h('div', this.$slots.default()),
// <div><slot name="footer" :text="message" /></div>
h(
'div',
this.$slots.footer({
text: this.message
})
)
]
}
}
Tương đương với JSX:
x
// <div><slot /></div>
<div>{this.$slots.default()}</div>
// <div><slot name="footer" :text="message" /></div>
<div>{this.$slots.footer({ text: this.message })}</div>
4.7 Truyền Slots
Truyền các thành phần con vào các component hoạt động khác biệt một chút so với truyền các thành phần con vào các phần tử. Thay vì một mảng, chúng ta cần truyền vào một hàm slot, hoặc một đối tượng của các hàm slot. Các hàm slot có thể trả về bất kỳ thứ gì mà một hàm render thông thường có thể trả về – điều này sẽ luôn được chuẩn hóa thành các mảng vnodes khi được truy cập trong component con.
// single default slot
h(MyComponent, () => 'hello')
// named slots
// notice the `null` is required to avoid
// the slots object being treated as props
h(MyComponent, null, {
default: () => 'default slot',
foo: () => h('div', 'foo'),
bar: () => [h('span', 'one'), h('span', 'two')]
})
Tương đương với JSX:
x
// default
<MyComponent>{() => 'hello'}</MyComponent>
// named
<MyComponent>{{
default: () => 'default slot',
foo: () => <div>foo</div>,
bar: () => [<span>one</span>, <span>two</span>]
}}</MyComponent>
Việc truyền slots dưới dạng hàm cho phép chúng được gọi một cách lười biếng bởi thành phần con. Điều này dẫn đến việc các phụ thuộc của slot được theo dõi bởi con cái thay vì cha mẹ, điều này dẫn đến cập nhật chính xác và hiệu quả hơn.
4.8 Scoped Slots
Để hiển thị một slot có phạm vi trong thành phần cha, một slot được truyền vào cho thành phần con. Chú ý cách slot bây giờ có một tham số text
. Slot sẽ được gọi trong thành phần con và dữ liệu từ thành phần con sẽ được truyền lên cho thành phần cha.
// parent component
export default {
setup() {
return () => h(MyComp, null, {
default: ({ text }) => h('p', text)
})
}
}
Nhớ truyền null
để các slot không được coi là props.
// child component
export default {
setup(props, { slots }) {
const text = ref('hi')
return () => h('div', null, slots.default({ text: text.value }))
}
}
Tương đương với JSX:
<MyComponent>{{
default: ({ text }) => <p>{ text }</p>
}}</MyComponent>
4.9 Các Thành Phần Tích Hợp Sẵn
Các thành phần tích hợp sẵn như <KeepAlive>
, <Transition>
, <TransitionGroup>
, <Teleport>
và <Suspense>
phải được nhập để sử dụng trong hàm render:
import { h, KeepAlive, Teleport, Transition, TransitionGroup } from 'vue'
export default {
setup () {
return () => h(Transition, { mode: 'out-in' }, /* ... */)
}
}
4.10v-model
Chỉ thị v-model
được mở rộng thành các props modelValue
và onUpdate:modelValue
trong quá trình biên dịch template—chúng ta sẽ phải cung cấp các props này một cách riêng theo logic model chúng ta:
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
setup(props, { emit }) {
return () =>
h(SomeComponent, {
modelValue: props.modelValue,
'onUpdate:modelValue': (value) => emit('update:modelValue', value)
})
}
}
4.11 Chỉ Thị Tùy Biến
Chỉ thị tùy biến có thể được áp dụng cho một vnode bằng cách sử dụng withDirectives:
import { h, withDirectives } from 'vue'
// a custom directive
const pin = {
mounted() { /* ... */ },
updated() { /* ... */ }
}
// <div v-pin:top.animate="200"></div>
const vnode = withDirectives(h('div'), [
[pin, 200, 'top', { animate: true }]
])
Nếu chỉ thị được đăng ký bằng tên và không thể được nhập trực tiếp, nó có thể được giải quyết bằng cách sử dụng trợ giúp resolveDirective.
4.12 Template Refs
Với Composition API, các template refs được tạo ra bằng cách truyền ref()
chính nó như một prop cho vnode:
import { h, ref } from 'vue'
export default {
setup() {
const divEl = ref()
// <div ref="divEl">
return () => h('div', { ref: divEl })
}
}
Với Options API, các template refs được tạo ra bằng cách truyền tên ref dưới dạng một chuỗi trong các props của vnode:
export default {
render() {
// <div ref="divEl">
return h('div', { ref: 'divEl' })
}
}
5. Các Thành Phần kiểu Hàm
Các thành phần hàm là một dạng thay thế của thành phần không có bất kỳ trạng thái nào riêng. Chúng hoạt động giống như các hàm thuần túy: props vào, vnode ra. Chúng được hiển thị mà không tạo ra một thể hiện thành phần (tức là không có this
), và không có các hook vòng đời thành phần thông thường.
Để tạo một thành phần hàm, chúng ta sử dụng một hàm thuần túy, thay vì một đối tượng tùy chọn. Hàm này thực ra là hàm render
cho thành phần.
Đăng ký một thành phần hàm giống như hook setup()
:
function MyComponent(props, { slots, emit, attrs }) {
// ...
}
Vì không có tham chiếu this
cho một thành phần hàm, Vue sẽ truyền vào props
là đối số đầu tiên:
function MyComponent(props, context) {
// ...
}
Đối số thứ hai, context
, chứa ba thuộc tính: attrs
, emit
, và slots
. Chúng tương đương với các thuộc tính của thể hiện $attrs, $emit, và $slots tương ứng.
Hầu hết các tùy chọn cấu hình thông thường cho các thành phần không khả dụng cho các thành phần hàm này. Tuy nhiên, có thể định nghĩa props và emits bằng cách thêm chúng làm thuộc tính:
MyComponent.props = ['value']
MyComponent.emits = ['click']
Nếu tùy chọn props
không được chỉ định, thì đối tượng props
được truyền vào hàm sẽ chứa tất cả các thuộc tính, giống như attrs
. Tên props sẽ không được chuẩn hóa thành camelCase trừ khi tùy chọn props
được chỉ định.
Đối với các thành phần hàm có props
cụ thể, sự thừa nhận thuộc tính hoạt động tương tự như với các thành phần bình thường. Tuy nhiên, đối với các thành phần hàm không rõ ràng chỉ định props
, chỉ có các class
, style
, và trình nghe sự kiện onXxx
sẽ được thừa kế từ attrs
theo mặc định. Trong cả hai trường hợp, inheritAttrs
có thể được đặt thành false
để vô hiệu hóa việc thừa kế thuộc tính:
MyComponent.inheritAttrs = false
Các thành phần hàm có thể được đăng ký và sử dụng giống như các thành phần bình thường. Nếu bạn truyền một hàm làm đối số đầu tiên cho h()
, nó sẽ được xử lý như một thành phần hàm.
5.1 Định Kiểu cho Thành Phần Hàm
Các Thành Phần Hàm có thể được định kiểu dựa trên việc chúng có tên hoặc vô danh. Vue – Tiện ích mở rộng chính thức cũng hỗ trợ kiểm tra loại các thành phần hàm được kiểu chính xác khi sử dụng chúng trong các mẫu SFC.
Thành Phần Hàm Có Tên
import type { SetupContext } from 'vue'
type FComponentProps = {
message: string
}
type Events = {
sendMessage(message: string): void
}
function FComponent(
props: FComponentProps,
context: SetupContext<Events>
) {
return (
<button onClick={() => context.emit('sendMessage', props.message)}>
{props.message} {' '}
</button>
)
}
FComponent.props = {
message: {
type: String,
required: true
}
}
FComponent.emits = {
sendMessage: (value: unknown) => typeof value === 'string'
}
Thành Phần Hàm Vô Danh(không tên)
x
import type { FunctionalComponent } from 'vue'
type FComponentProps = {
message: string
}
type Events = {
sendMessage(message: string): void
}
const FComponent: FunctionalComponent<FComponentProps, Events> = (
props,
context
) => {
return (
<button onClick={() => context.emit('sendMessage', props.message)}>
{props.message} {' '}
</button>
)
}
FComponent.props = {
message: {
type: String,
required: true
}
}
FComponent.emits = {
sendMessage: (value) => typeof value === 'string'
}
Trên đây là một cái nhìn tổng quan về Vuejs với Render Functions & JSX. Hy vọng rằng bạn đã có cái nhìn sâu hơn về cách sử dụng Render Functions và JSX trong dự án Vue của mình. Đừng ngần ngại áp dụng những kiến thức này vào công việc thực tế của bạn và khám phá thêm về sức mạnh của Vuejs. Hãy tiếp tục theo dõi Cafedev để cập nhật những bài viết mới nhất về công nghệ và lập trình!
Tham khảo thêm: MIỄN PHÍ 100% | Series tự học Vuejs từ cơ bản tới nâng cao
Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây
Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của CafeDev để nhận được nhiều hơn nữa:
Chào thân ái và quyết thắng!