Skip to content
On this page

Vue.js 模板语法

Vue.js 提供了一套基于HTML的模板语法,允许我们声明式地将渲染后的数据绑定到DOM中。

插值

文本插值

使用 双大括号语法进行文本插值:

vue
<template>
  <div>
    <!-- 基本文本插值 -->
    <p>消息: {{ message }}</p>
    
    <!-- JavaScript 表达式 -->
    <p>{{ number + 1 }}</p>
    <p>{{ ok ? 'YES' : 'NO' }}</p>
    <p>{{ message.split('').reverse().join('') }}</p>
    
    <!-- 带有修饰符的插值 -->
    <p>{{ { a: 1, b: 2 } }}</p> <!-- 会转换为字符串 -->
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello Vue!',
      number: 10,
      ok: true
    }
  }
}
</script>

原始 HTML

使用 v-html 指令输出原始HTML内容:

vue
<template>
  <div>
    <!-- 普通插值,HTML会被转义 -->
    <p>{{ rawHtml }}</p>
    
    <!-- v-html 输出原始HTML -->
    <div v-html="rawHtml"></div>
    
    <!-- 使用插槽作为更安全的替代方案 -->
    <div><slot name="content"></slot></div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      rawHtml: '<span style="color: red">这是红色文本</span>'
    }
  }
}
</script>

特性绑定

使用 v-bind 指令绑定HTML特性:

vue
<template>
  <div>
    <!-- 动态特性绑定 -->
    <img v-bind:src="imageSrc" v-bind:alt="imageAlt">
    
    <!-- 简写形式 -->
    <img :src="imageSrc" :alt="imageAlt">
    
    <!-- 动态特性名 -->
    <button v-bind:[dynamicAttribute]="attributeValue">动态特性</button>
    
    <!-- 绑定对象 -->
    <div v-bind="{ id: staticId, class: className, 'data-value': dataValue }">
      绑定对象示例
    </div>
    
    <!-- 绑定对象(简写) -->
    <div v-bind="objectOfAttrs">
      对象属性绑定
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      imageSrc: '/path/to/image.jpg',
      imageAlt: '图片描述',
      dynamicAttribute: 'title',
      attributeValue: '动态标题',
      staticId: 'my-id',
      className: 'my-class',
      dataValue: 'data-value',
      objectOfAttrs: {
        id: 'container',
        class: 'wrapper',
        'data-info': 'info'
      }
    }
  }
}
</script>

指令

v-text

更新元素的文本内容:

vue
<template>
  <div>
    <!-- 等价于 {{ text }} -->
    <span v-text="textContent"></span>
    
    <!-- 会覆盖元素内的任何现有内容 -->
    <p v-text="textContent">这段文字会被覆盖</p>
    
    <!-- 普通插值不会覆盖 -->
    <p>这段文字不会被覆盖 {{ textContent }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      textContent: '这是动态文本内容'
    }
  }
}
</script>

v-html

更新元素的innerHTML:

vue
<template>
  <div>
    <!-- 安全的文本插值 -->
    <p>{{ rawHtml }}</p>
    
    <!-- 危险的HTML插入,需要确保内容安全 -->
    <div v-html="rawHtml"></div>
    
    <!-- 使用Sanitizer API(如果支持) -->
    <div ref="htmlContainer"></div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      rawHtml: '<p style="color: blue;">这是<em>HTML</em>内容</p>'
    }
  },
  mounted() {
    // 使用Sanitizer API(如果浏览器支持)
    if (window.Sanitizer) {
      this.$refs.htmlContainer.setHTML(this.rawHtml, { sanitizer: new Sanitizer() })
    }
  }
}
</script>

v-show

条件性地显示或隐藏元素:

vue
<template>
  <div>
    <!-- 基本用法 -->
    <p v-show="showMessage">这是一条消息</p>
    
    <!-- 表达式 -->
    <p v-show="counter > 5">计数器大于5时显示</p>
    
    <!-- 复杂条件 -->
    <div v-show="isVisible && isActive && !isDisabled">
      复杂条件显示
    </div>
    
    <!-- 控制按钮 -->
    <button @click="toggleMessage">切换消息显示</button>
    <button @click="increment">增加计数</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      showMessage: true,
      counter: 0,
      isVisible: true,
      isActive: true,
      isDisabled: false
    }
  },
  methods: {
    toggleMessage() {
      this.showMessage = !this.showMessage
    },
    increment() {
      this.counter++
    }
  }
}
</script>

v-if, v-else, v-else-if

条件性地渲染元素:

vue
<template>
  <div>
    <!-- 基本条件渲染 -->
    <h1 v-if="type === 'A'">A</h1>
    <h1 v-else-if="type === 'B'">B</h1>
    <h1 v-else>C</h1>
    
    <!-- 复杂条件 -->
    <div v-if="user.loggedIn">
      <p>欢迎, {{ user.name }}!</p>
      <button @click="logout">登出</button>
    </div>
    <div v-else>
      <p>请先登录</p>
      <button @click="login">登录</button>
    </div>
    
    <!-- 在组件上使用 -->
    <UserComponent v-if="user.loggedIn" :user="user" />
    <LoginComponent v-else @login="handleLogin" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      type: 'A',
      user: {
        loggedIn: false,
        name: '张三'
      }
    }
  },
  methods: {
    logout() {
      this.user.loggedIn = false
    },
    login() {
      this.user.loggedIn = true
    },
    handleLogin(userData) {
      this.user = { ...this.user, ...userData, loggedIn: true }
    }
  }
}
</script>

v-for

列表渲染:

vue
<template>
  <div>
    <!-- 遍历数组 -->
    <ul>
      <li v-for="item in items" :key="item.id">
        {{ item.name }} - {{ item.price }}
      </li>
    </ul>
    
    <!-- 带索引的遍历 -->
    <ul>
      <li v-for="(item, index) in items" :key="item.id">
        {{ index + 1 }}. {{ item.name }}
      </li>
    </ul>
    
    <!-- 遍历对象 -->
    <ul>
      <li v-for="(value, key) in userProfile" :key="key">
        {{ key }}: {{ value }}
      </li>
    </ul>
    
    <!-- 遍历数字 -->
    <span v-for="n in 10" :key="n">{{ n }} </span>
    
    <!-- 在组件上使用 -->
    <TodoItem
      v-for="todo in todos"
      :key="todo.id"
      :todo="todo"
      @complete="completeTodo"
    />
    
    <!-- 解构v-for -->
    <div v-for="{ id, name, age } in users" :key="id">
      <p>{{ name }} - {{ age }}</p>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: [
        { id: 1, name: '苹果', price: 5 },
        { id: 2, name: '香蕉', price: 3 },
        { id: 3, name: '橙子', price: 4 }
      ],
      userProfile: {
        name: '张三',
        age: 30,
        email: 'zhangsan@example.com'
      },
      todos: [
        { id: 1, text: '学习Vue', completed: false },
        { id: 2, text: '写代码', completed: true }
      ],
      users: [
        { id: 1, name: '张三', age: 25 },
        { id: 2, name: '李四', age: 30 }
      ]
    }
  },
  methods: {
    completeTodo(todoId) {
      const todo = this.todos.find(t => t.id === todoId)
      if (todo) {
        todo.completed = true
      }
    }
  }
}
</script>

v-on (事件处理)

绑定事件监听器:

vue
<template>
  <div>
    <!-- 基本事件处理 -->
    <button v-on:click="counter++">计数器: {{ counter }}</button>
    
    <!-- 简写形式 -->
    <button @click="greet">打招呼</button>
    
    <!-- 方法调用 -->
    <button @click="say('hi')">说hi</button>
    
    <!-- 内联语句 -->
    <button @click="counter += 2">增加2</button>
    
    <!-- 传参 -->
    <button @click="addItem('新项目')">添加项目</button>
    
    <!-- 事件修饰符 -->
    <!-- .stop - 阻止事件冒泡 -->
    <div @click="outerClick">
      外层
      <button @click.stop="innerClick">内层 - 不会冒泡</button>
    </div>
    
    <!-- .prevent - 阻止默认行为 -->
    <form @submit.prevent="onSubmit">
      <input type="text" v-model="inputValue">
      <button type="submit">提交</button>
    </form>
    
    <!-- .capture - 使用事件捕获 -->
    <div @click.capture="captureClick">
      捕获模式
      <button @click="normalClick">普通点击</button>
    </div>
    
    <!-- .self - 只在事件目标是元素本身时触发 -->
    <div @click.self="selfClick">
      只在点击自身时触发
      <button @click="buttonClick">按钮</button>
    </div>
    
    <!-- .once - 事件只触发一次 -->
    <button @click.once="onceClick">只触发一次</button>
    
    <!-- .passive - 以被动模式添加事件监听器 -->
    <div @scroll.passive="onScroll">滚动区域</div>
    
    <!-- 按键修饰符 -->
    <input @keyup.enter="onEnter" placeholder="按Enter键触发">
    <input @keyup.esc="onEscape" placeholder="按ESC键触发">
    <input @keyup.space="onSpace" placeholder="按空格键触发">
    <input @keyup.up="onArrowUp" placeholder="按上箭头键触发">
    
    <!-- 系统修饰键 -->
    <div @click.ctrl="onCtrlClick">按住Ctrl点击</div>
    <div @click.shift="onShiftClick">按住Shift点击</div>
    <div @click.alt="onAltClick">按住Alt点击</div>
    <div @click.meta="onMetaClick">按住Meta键(如Cmd)点击</div>
    
    <!-- 鼠标修饰符 -->
    <div @click.left="onLeftClick">左键点击</div>
    <div @click.right="onRightClick">右键点击</div>
    <div @click.middle="onMiddleClick">中键点击</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      counter: 0,
      inputValue: '',
      message: 'Hello'
    }
  },
  methods: {
    greet() {
      alert(`Hello, ${this.message}!`)
    },
    say(message) {
      alert(message)
    },
    addItem(item) {
      console.log('添加项目:', item)
    },
    outerClick() {
      console.log('外层点击')
    },
    innerClick() {
      console.log('内层点击')
    },
    onSubmit() {
      console.log('表单提交被阻止')
    },
    captureClick() {
      console.log('捕获阶段触发')
    },
    normalClick() {
      console.log('普通点击')
    },
    selfClick() {
      console.log('只在点击自身时触发')
    },
    buttonClick() {
      console.log('按钮点击')
    },
    onceClick() {
      console.log('只触发一次')
    },
    onScroll() {
      console.log('滚动事件')
    },
    onEnter() {
      console.log('按下了Enter键')
    },
    onEscape() {
      console.log('按下了Escape键')
    },
    onSpace() {
      console.log('按下了空格键')
    },
    onArrowUp() {
      console.log('按下了上箭头键')
    },
    onCtrlClick() {
      console.log('按住Ctrl点击')
    },
    onShiftClick() {
      console.log('按住Shift点击')
    },
    onAltClick() {
      console.log('按住Alt点击')
    },
    onMetaClick() {
      console.log('按住Meta键点击')
    },
    onLeftClick() {
      console.log('左键点击')
    },
    onRightClick() {
      console.log('右键点击')
    },
    onMiddleClick() {
      console.log('中键点击')
    }
  }
}
</script>

v-model (双向数据绑定)

在表单元素上创建双向数据绑定:

vue
<template>
  <div>
    <!-- 文本输入 -->
    <div>
      <input v-model="message" placeholder="输入文本">
      <p>消息: {{ message }}</p>
    </div>
    
    <!-- 多行文本 -->
    <div>
      <textarea v-model="textareaValue" placeholder="多行文本"></textarea>
      <p>多行文本: {{ textareaValue }}</p>
    </div>
    
    <!-- 复选框 -->
    <div>
      <input type="checkbox" id="checkbox" v-model="checked">
      <label for="checkbox">{{ checked ? '已选中' : '未选中' }}</label>
    </div>
    
    <!-- 多个复选框 -->
    <div>
      <input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
      <label for="jack">Jack</label>
      <input type="checkbox" id="john" value="John" v-model="checkedNames">
      <label for="john">John</label>
      <input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
      <label for="mike">Mike</label>
      <br>
      <span>选中: {{ checkedNames }}</span>
    </div>
    
    <!-- 单选按钮 -->
    <div>
      <input type="radio" id="one" value="One" v-model="picked">
      <label for="one">One</label>
      <input type="radio" id="two" value="Two" v-model="picked">
      <label for="two">Two</label>
      <br>
      <span>选中: {{ picked }}</span>
    </div>
    
    <!-- 选择框 -->
    <div>
      <select v-model="selected">
        <option disabled value="">请选择</option>
        <option>A</option>
        <option>B</option>
        <option>C</option>
      </select>
      <span>选中: {{ selected }}</span>
    </div>
    
    <!-- 多选选择框 -->
    <div>
      <select v-model="multiSelected" multiple>
        <option>A</option>
        <option>B</option>
        <option>C</option>
      </select>
      <span>选中: {{ multiSelected }}</span>
    </div>
    
    <!-- v-model 修饰符 -->
    <!-- .lazy - 在 change 事件而不是 input 事件中更新 -->
    <div>
      <input v-model.lazy="lazyValue" placeholder="在change事件中更新">
      <p>延迟更新值: {{ lazyValue }}</p>
    </div>
    
    <!-- .number - 自动将输入值转换为数字 -->
    <div>
      <input v-model.number="age" type="number" placeholder="输入年龄">
      <p>年龄类型: {{ typeof age }}</p>
    </div>
    
    <!-- .trim - 自动过滤输入值的首尾空格 -->
    <div>
      <input v-model.trim="trimmedValue" placeholder="输入带空格的内容">
      <p>修剪后值: "{{ trimmedValue }}"</p>
    </div>
    
    <!-- 在组件上使用v-model -->
    <CustomInput v-model="customValue" />
    <p>自定义组件值: {{ customValue }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: '',
      textareaValue: '',
      checked: false,
      checkedNames: [],
      picked: '',
      selected: '',
      multiSelected: [],
      lazyValue: '',
      age: 0,
      trimmedValue: '',
      customValue: ''
    }
  }
}
</script>

动态参数

使用方括号语法使用动态参数:

vue
<template>
  <div>
    <!-- 动态特性名 -->
    <div v-bind:[dynamicAttribute]="attributeValue">
      动态特性绑定
    </div>
    
    <!-- 动态事件名 -->
    <button v-on:[dynamicEvent]="handleDynamicEvent">
      动态事件
    </button>
    
    <!-- 动态参数的修饰符 -->
    <input :[dynamicAttribute].camel="camelValue">
    
    <!-- 按钮用于测试 -->
    <button @click="changeDynamicAttribute">改变动态特性</button>
    <button @click="changeDynamicEvent">改变动态事件</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      dynamicAttribute: 'title',
      attributeValue: '动态标题',
      dynamicEvent: 'click',
      camelValue: 'camelCase值'
    }
  },
  methods: {
    handleDynamicEvent() {
      console.log('动态事件被触发')
    },
    changeDynamicAttribute() {
      this.dynamicAttribute = this.dynamicAttribute === 'title' ? 'data-info' : 'title'
      this.attributeValue = `新值 ${Date.now()}`
    },
    changeDynamicEvent() {
      this.dynamicEvent = this.dynamicEvent === 'click' ? 'dblclick' : 'click'
    }
  }
}
</script>

修饰符

事件修饰符

vue
<template>
  <div>
    <!-- 阻止事件冒泡 -->
    <div @click="outerClick" style="padding: 20px; background: lightblue;">
      外层
      <button @click.stop="innerClick" style="margin: 10px;">内层按钮 - 阻止冒泡</button>
    </div>
    
    <!-- 阻止默认行为 -->
    <form @submit.prevent="handleSubmit" style="margin: 10px;">
      <input type="text" placeholder="表单输入">
      <button type="submit">提交(默认行为被阻止)</button>
    </form>
    
    <!-- 修饰符串联 -->
    <button @click.stop.prevent="bothAction">既阻止冒泡又阻止默认行为</button>
    
    <!-- 只在事件目标是元素本身时触发 -->
    <div @click.self="selfOnly" style="padding: 20px; background: lightgreen;">
      只在点击此div本身时触发
      <p>点击内部元素不会触发</p>
      <button>按钮</button>
    </div>
    
    <!-- 事件只触发一次 -->
    <button @click.once="onceAction">只触发一次</button>
    
    <!-- 捕获模式 -->
    <div @click.capture="capturePhase" style="padding: 10px; background: lightcoral;">
      捕获阶段
      <button @click="bubblePhase">冒泡阶段</button>
    </div>
  </div>
</template>

<script>
export default {
  methods: {
    outerClick() {
      console.log('外层点击')
    },
    innerClick() {
      console.log('内层点击')
    },
    handleSubmit() {
      console.log('表单提交被阻止')
    },
    bothAction() {
      console.log('阻止冒泡和默认行为')
    },
    selfOnly() {
      console.log('只在点击自身时触发')
    },
    onceAction() {
      console.log('只触发一次')
    },
    capturePhase() {
      console.log('捕获阶段触发')
    },
    bubblePhase() {
      console.log('冒泡阶段触发')
    }
  }
}
</script>

按键修饰符

vue
<template>
  <div>
    <!-- 普通按键 -->
    <input @keyup.enter="onEnter" placeholder="按Enter键">
    <input @keyup.space="onSpace" placeholder="按空格键">
    <input @keyup.esc="onEscape" placeholder="按ESC键">
    
    <!-- 方向键 -->
    <input @keyup.up="onArrowUp" placeholder="按上箭头">
    <input @keyup.down="onArrowDown" placeholder="按下箭头">
    <input @keyup.left="onArrowLeft" placeholder="按左箭头">
    <input @keyup.right="onArrowRight" placeholder="按右箭头">
    
    <!-- 系统修饰键 -->
    <input @keyup.ctrl="onCtrl" placeholder="按Ctrl键">
    <input @keyup.alt="onAlt" placeholder="按Alt键">
    <input @keyup.shift="onShift" placeholder="按Shift键">
    <input @keyup.meta="onMeta" placeholder="按Meta键(Cmd)">
    
    <!-- 组合键 -->
    <input @keyup.ctrl.a="onCtrlA" placeholder="按Ctrl+A">
    <input @keyup.ctrl."="="onCtrlPlus" placeholder="按Ctrl+Plus">
    
    <!-- 系统修饰键组合 -->
    <div @keyup.ctrl.exact="onCtrlOnly">只按Ctrl键时触发</div>
    <div @keyup.exact="onNoneModifier">没有任何系统修饰键时触发</div>
    
    <!-- 鼠标修饰符 -->
    <div @click.left="onLeftClick">左键点击</div>
    <div @click.right="onRightClick">右键点击</div>
    <div @click.middle="onMiddleClick">中键点击</div>
  </div>
</template>

<script>
export default {
  methods: {
    onEnter() { console.log('Enter键') },
    onSpace() { console.log('空格键') },
    onEscape() { console.log('ESC键') },
    onArrowUp() { console.log('上箭头') },
    onArrowDown() { console.log('下箭头') },
    onArrowLeft() { console.log('左箭头') },
    onArrowRight() { console.log('右箭头') },
    onCtrl() { console.log('Ctrl键') },
    onAlt() { console.log('Alt键') },
    onShift() { console.log('Shift键') },
    onMeta() { console.log('Meta键') },
    onCtrlA() { console.log('Ctrl+A') },
    onCtrlPlus() { console.log('Ctrl+Plus') },
    onCtrlOnly() { console.log('只按Ctrl键') },
    onNoneModifier() { console.log('没有任何修饰键') },
    onLeftClick() { console.log('左键点击') },
    onRightClick() { console.log('右键点击') },
    onMiddleClick() { console.log('中键点击') }
  }
}
</script>

组件上的 v-model

vue
<!-- CustomInput.vue -->
<template>
  <div class="custom-input">
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      :class="{ error: hasError }"
      v-bind="$attrs"
    >
    <span v-if="hasError" class="error-message">{{ errorMessage }}</span>
  </div>
</template>

<script>
export default {
  name: 'CustomInput',
  props: {
    modelValue: String,
    validator: Function
  },
  emits: ['update:modelValue'],
  computed: {
    hasError() {
      return this.validator && !this.validator(this.modelValue)
    },
    errorMessage() {
      return this.hasError ? '输入不合法' : ''
    }
  }
}
</script>

<!-- 父组件使用 -->
<template>
  <div>
    <h3>组件上的 v-model</h3>
    <CustomInput 
      v-model="inputValue"
      :validator="validateInput"
      placeholder="请输入至少3个字符"
    />
    <p>输入值: {{ inputValue }}</p>
  </div>
</template>

<script>
import CustomInput from './CustomInput.vue'

export default {
  components: {
    CustomInput
  },
  data() {
    return {
      inputValue: ''
    }
  },
  methods: {
    validateInput(value) {
      return value && value.length >= 3
    }
  }
}
</script>

v-slot (插槽)

默认插槽和具名插槽

vue
<!-- Card.vue -->
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header">默认头部</slot>
    </div>
    <div class="card-body">
      <slot>默认内容</slot>
    </div>
    <div class="card-footer">
      <slot name="footer">默认底部</slot>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Card'
}
</script>

<style scoped>
.card {
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
}

.card-header, .card-footer {
  background-color: #f8f9fa;
  padding: 12px 16px;
}

.card-body {
  padding: 16px;
}
</style>
vue
<!-- 使用插槽 -->
<template>
  <div>
    <Card>
      <template #header>
        <h3>自定义头部</h3>
      </template>
      
      <p>这是自定义内容</p>
      
      <template #footer>
        <button>操作按钮</button>
      </template>
    </Card>
  </div>
</template>

<script>
import Card from './Card.vue'

export default {
  components: {
    Card
  }
}
</script>

作用域插槽

vue
<!-- UserList.vue -->
<template>
  <div class="user-list">
    <div 
      v-for="(user, index) in users" 
      :key="user.id" 
      class="user-item"
    >
      <slot 
        :user="user" 
        :index="index" 
        :isEven="index % 2 === 0"
        :isActive="user.active"
      >
        <!-- 默认插槽内容 -->
        <span>{{ user.name }}</span>
      </slot>
    </div>
  </div>
</template>

<script>
export default {
  name: 'UserList',
  props: {
    users: {
      type: Array,
      default: () => []
    }
  }
}
</script>
vue
<!-- 使用作用域插槽 -->
<template>
  <div>
    <UserList :users="users">
      <template #default="{ user, index, isEven, isActive }">
        <div :class="['user-item', { even: isEven, active: isActive }]">
          <strong>{{ user.name }}</strong> - {{ user.email }}
          <span class="index">(#{{ index + 1 }})</span>
          <span v-if="isActive" class="status">活跃</span>
        </div>
      </template>
    </UserList>
  </div>
</template>

<script>
import UserList from './UserList.vue'

export default {
  components: {
    UserList
  },
  data() {
    return {
      users: [
        { id: 1, name: '张三', email: 'zhangsan@example.com', active: true },
        { id: 2, name: '李四', email: 'lisi@example.com', active: false },
        { id: 3, name: '王五', email: 'wangwu@example.com', active: true }
      ]
    }
  }
}
</script>

Vue.js 的模板语法提供了一套简洁而强大的声明式渲染机制,通过这些语法可以轻松地将数据渲染到DOM中。