vite知识点

373 阅读1分钟

TypeScript 编译器选项:isolatedModules

作用:确保每个文件在编译时都是独立的,启用后,从非模块文件(如没有明确的导出的js文件)中导入内容TypeScript会报错

// nonModuleFile.js
const myVariable = "Hello from non-module file!";
// moduleFile.ts
import { myVariable } from './nonModuleFile.js'; 
// 这里的import会报错,因为 nonModuleFile.js 是非模块文件
console.log(myVariable);

css预处理器和postcss区别

css预处理器:扩展了普通的css语法,便于维护和编写css样式

postcss:主要用来处理css,提供了各种插件来执行不同的转换和优化。比如使用 Autoprefixer  PostCSS 插件,可以根据最新的浏览器兼容性数据自动为 CSS 属性添加所需的前缀。

css模块化

/* example.module.css */
.red {
  color: red;
}
//xxx.js
import classes from './example.module.css'
document.getElementById('foo').className = classes.red

@import的内联

vite利用postcss-import预配置来处理css的@import内联。

before:@import会导致浏览器发起额外的网络请求去加载被引入的外部 CSS 文件

now:配置为内联模式,可以将被 @import 引入的外部 CSS 文件的内容直接合并到当前的 CSS 文件中,而不发起额外的网络请求

配置:

//安装postcss-import
yarn add -D postcss-import
//在vite.config.ts中引入
import { defineConfig } from 'vite';
import postcssImport from 'postcss-import';
export default defineConfig({
  plugins: [
    postcssImport()
  ]
});



//variables.css
:root {
  --primary-color: #ff0000;
}
//main.css
@import 'variables.css';
body {
  background-color: var(--primary-color);
}

//最终生成的css
:root {
  --primary-color: #ff0000;
}

body {
  background-color: var(--primary-color);
}

css的@import变基

变基的概念:将导入的文件路径重新基于当前文件的路径或根路径。确保导入的文件能够正确地被定位到,而不受其在项目文件结构中的实际位置的影响

//项目目录
project-root
├── src
│   ├── styles
│   │   ├── main.css
│   │   └── variables.css
└── vite.config.js

/* main.css */
@import 'variables.css';

假设在 Vite 的配置中将路径别名 @ 映射到 src 目录,那么 @import 'variables.css'; 实际上应该被解析为 @import '@/styles/variables.css';,才能正确地引入 variables.css 文件。手动处理的话后期维护会比较麻烦。

css注入页面

定义:CSS 内容自动注入到 HTML 中

import './foo.css' // 样式将会注入页面import otherStyles from './bar.css?inline' // 样式不会注入页面

自 Vite 5 起,CSS 文件的默认导入和按名导入(例如 import style from './foo.css')会被移除。使用 ?inline 参数代替。

静态资源处理

正常情况下,导入一个静态资源会返回解析后的 URL,但在vite中添加查询参数可以更改资源被引入的方式

import imgUrl from './img.png'
document.getElementById('hero-img').src = imgUrl

// 显式加载资源为一个 URL
import assetAsURL from './asset.js?url'
// 以字符串形式加载资源
import assetAsString from './shader.glsl?raw'
// 加载为 Web Worker
import Worker from './worker.js?worker'
// 在构建时 Web Worker 内联为 base64 字符串
import InlineWorker from './worker.js?worker&inline'

情景配置

概念:需要基于不同环境(dev/serve,build)来决定配置

export default defineConfig(({ command, mode, isSsrBuild, isPreview }) => {
  if (command === 'serve') {
    return {
      // dev 独有配置
    }
  } else {
    // command === 'build'
    return {
      // build 独有配置
    }
  }
})

环境变量

获取:import.meta.env

vite默认不加载.env文件,原因是需要在执行完vite配置后才能确定加载哪一个。但如果需要的话可以用loadEnv函数加载指定的环境文件

import { defineConfig, loadEnv } from 'vite'

export default defineConfig(({ command, mode }) => {
  // 根据当前工作目录中的 `mode` 加载 .env 文件
  // 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。
  const env = loadEnv(mode, process.cwd(), '')
  return {
    // vite 配置
    define: {
      __APP_ENV__: JSON.stringify(env.APP_ENV),
    },
  }
})

Property 'env' does not exist on type 'ImportMeta'.ts(2339)

解决方法:在ts配置文件的include配置项数组中添加

"include": ["env.d.ts", "src/**/*", "src/**/*.vue"]

Vue

模板语法

  1. 文本插值:<span>Message: {{ msg }}</span>

  2. 原始html: <span v-html="rawHtml"></span>

  3. Attribute绑定:<div :id="dynamicId"></div>   【同名】<div :id></div>

  4. JS表达式:{{ ok ? 'YES' : 'NO' }},仅仅支持表达式,不支持语句,比如{{ if (ok) { return message } }}

  5. 指令:<p v-if="seen">Now you see me</p>

  6. 动态参数:<a :[attributeName]="url"> ... </a>

  7. 动态参数式中,表达式的值应该是一个字符串或者null(显示移除该绑定)

  8. Attribute不能大写,它会在html模板中转换为全小写。单文件组件内容的模板不受限制

  9. 修饰符:以点开头的特殊后缀,表明指令需要以一些特殊的方式被绑定<form @submit.prevent="onSubmit">...</form>

指令语法图

响应式

推荐的声明响应式状态的API:ref(),

具有深层响应性,即使改变嵌套对象或数组时,变化也会被检测到(可以通过shallow ref 放弃,可以用于避免对大型数据的响应性开销来优化性能)

定义:const count = ref(0)

获取:count.value

在模板中使用ref的时候不需要添加.value,它会自动解包,但是在js中需要强制加上

<template>
 <div>{{ count }}</div>
</template>
<script>
import { ref } from 'vue'
export default {
  setup() {
    const count = ref(0)
    function increment() {
      // 在 JavaScript 中需要 .value
      count.value++
    }
    // 将 ref 暴露给模板
    return {
      count
    }
  }
}
</script>

缺点:在setup函数中手动暴露state或者function非常繁琐,可以通过单文件组件()

<script setup> 
import { ref } from 'vue'
const count = ref(0) 
function increment() { count.value++ } 
</script> 
<template> 
<button @click="increment"> {{ count }} </button> 
</template>

原理:

基于依赖追踪的响应式系统实现,在模板中使用了一个ref并改变了值的时候,vue会检测到,并且更新对应的DOM。

首次渲染:Vue追踪在渲染过程中用到的每一个ref对象

ref值被修改:Vue触发追踪它的组件的一次重新渲染

普通js中普通变量的访问和修改是检测不到的,所以需要通过getter和setter方法来拦截对象属性的get和set。.value属性让Vue可以检测到ref被访问或者修改。Vue在getter中执行追踪,在setter中执行触发。可以将ref看成:

// 伪代码
const myRef = {
  _value: 0,
  get value() {
    track()
    return this._value
  },
  set value(newValue) {
    this._value = newValue
    trigger()
  }
}

同时另外一方面还可以将ref传给函数,同时保留对最新值和响应式连接的访问。代码重构时会非常有用。

DOM更新时机

非同步,Vue会在“next tick”更新周期中缓冲所有状态的修改,在多次修改状态的情况下,每个组件都只会被更新一次。

要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API:

import { nextTick } from 'vue'

async function increment() {
  count.value++
  await nextTick()
  // 现在 DOM 已经更新了
}

reactive:

ref是将内部值包裹在特殊对象中,拦截对响应式对象所有属性的访问和修改,以便进行依赖追踪和触发更新。

reactive是让对象本身具有响应性。深层转换对象:当访问嵌套对象的时候,它们也会被reactive包装。当

import { reactive } from 'vue'
const state = reactive({ count: 0 })

reactive()返回的是一个原始对象的proxy,和原始对象并不相等。

const raw = {}
const proxy = reactive(raw)
console.log(proxy === raw) //false
// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true

即:只有代理对象是响应式的,修改原始对象不会触发更新。

reactive的劣势:

  1. 只能用于对象类型,不能用于原始类型,而ref都支持

  2. 不能替换整个对象,必须保持对响应式对象的相同引用

    let state = reactive({ count: 0 })
    // 上面的 ({ count: 0 }) 引用将不再被追踪
    // (响应性连接已丢失!)
    state = reactive({ count: 1 })
    
  3. 对解构操作不友好:将响应对象的原始类型属性结构为本地变量或者将该属性传递给函数会都是响应性连接,而ref解构后不会丢失响应性

    const state = reactive({ count: 0 })
    // 当解构时,count 已经与 state.count 断开连接
    let { count } = state
    // 不会影响原始的 state
    count++
    
    // 该函数接收到的是一个普通的数字
    // 并且无法追踪 state.count 的变化
    // 我们必须传入整个对象以保持响应性
    callSomeFunction(state.count)
    

解包

在模板渲染上下文中,只有顶级的 ref 属性才会被解包。

const count = ref(0)
const object = { id: ref(1) }
{{ count + 1 }}//正常
{{ object.id + 1 }}//[object Object]1
//修复:
const { id } = object
{{id+1}}

计算属性computed

作用:描述依赖响应式状态的复杂逻辑

const publishedBooksMessage = computed(() => {  return author.books.length > 0 ? 'Yes' : 'No'})

传入一个getter函数,返回一个计算属性ref。可以通过xx.value获取值,在模板中也会自动解包。

会自动追踪响应式依赖,一旦依赖的状态变化,依赖于状态的computed属性值也会产生变化。

陷阱:计算属性只有在依赖的状态发生变化时才会更新,比如下面这个计算属性永远不会更新,因为它没有依赖的状态变量

const now = computed(() => Date.now())

computed vs method:

计算属性的 getter 应只做计算而没有任何其他的副作用

计算属性值会基于其响应式依赖被缓存,只有依赖变化时,计算属性才会重新计算并更新,不会重复执行。而方法总是会在重渲染发生时再次执行函数。

不适合在computed中执行的操作:

  1. 改变其他的响应式数据
  2. 在getter中做异步请求或者修改DOM

类和样式绑定

class绑定

传递一个内联字面量对象

//class和动态的class可以共存
:class="{active:isActive,'text-danger': hasError}"

传递一个响应式对象

const classObject = reactive({
  active: true,
  'text-danger': false
})
<div :class="classObject"></div>

传递一个computed对象

const isActive = ref(true)
const error = ref(null)

const classObject = computed(() => ({
  active: isActive.value && !error.value,
  'text-danger': error.value && error.value.type === 'fatal'
}))
<div :class="classObject"></div>

绑定一个数组渲染多个class

const activeClass = ref('active')
const errorClass = ref('text-danger')
<div :class="[activeClass, errorClass]"></div>

父组件如果设置了class:

子组件的唯一根元素也设置了class,则2者会自动合并。

<!-- 子组件模板MyComponent -->
<p class="foo bar">Hi!</p>
<!-- 在使用组件时 -->
<MyComponent class="baz boo" />
//最终结果
<p class="foo bar baz boo">Hi!</p>

子组件有多个根元素,则通过$attrs指定接受的元素

<!-- 子组件模板MyComponent  -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>
<!-- 在使用组件时 -->
<MyComponent class="baz" />
//最终结果
<p class="baz">Hi!</p>
<span>This is a child component</span>

style绑定

传入js对象

const activeColor = ref('red')
const fontSize = ref(30)
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

传入一个响应式对象

const styleObject = reactive({
  color: 'red',
  fontSize: '13px'
})
<div :style="styleObject"></div>

传入computed属性
绑定数组

<div :style="[baseStyles, overridingStyles]"></div>

如果需要传入不同前缀的浏览器特性:

<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

条件渲染

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

v-if:内容只会在指令的表达式返回真值时才被渲染

v-else:必须跟在v-if或者v-else-if后面

可在template元素上使用,但最后渲染结果并不会包含template元素

---

v-show:内容只会在指令的表达式返回真值时才显示

二者区别:

  1. v-show只是切换了绑定的display属性,但在DOM中还是会渲染这个元素,初始渲染开销高。v-if是真正修改了DOM元素的渲染,对应的事件监听器和子组件都会被销毁和重建,切换开销高
  2. v-show不支持绑定template元素,但v-if可以

禁忌:v-if不推荐和v-for一起使用,因为v-if会首先被执行,

当v-if和v-for存在于同一个节点,v-if优先级更高,导致v-if中获取不到v-for作用域内定义的变量别名

//这会抛出一个错误,因为属性 todo 此时没有在该实例上定义
<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo.name }}
</li>

列表渲染

基础使用

<li v-for="(item, index) in items">
  {{ index }} - {{ item.message }}
</li>

<div v-for="item of items">{{ item.message }}</div>

结构

<li v-for="({ message }, index) in items">
  {{ message }} {{ index }}
</li>

嵌套列表

<li v-for="item in items">
  <span v-for="childItem in item.children">
    {{ item.message }} {{ childItem }}
  </span>
</li>

遍历对象

const myObject = reactive({
  title: 'How to do lists in Vue',
  author: 'Jane Doe',
  publishedAt: '2016-04-10'
})
<li v-for="(value, key, index) in myObject">
  {{ index }}. {{ key }}: {{ value }}
</li>

范围值:整数,n从1开始

<span v-for="n in 10">{{ n }}</span>

v-for也可以绑定在template元素上,用来渲染一个包含多个元素的块

<div v-for="item in items" :key="item.id">
</div>

v-for需要设置key的作用:

Vue默认是通过就地更新的策略更新v-for渲染的列表。即当items数据顺序发生变化,dom的顺序结构不改变,只改变dom上相关的data或者属性,提高渲染性能。这对于输出结果不依赖于子组件状态或者临时DOM状态非常有用

<script setup>import { reactive} from 'vue'const dataList = reactive([    { name: 'xiaomi', goodsId: '12'},    { name: 'apple', goodsId: '13'},    { name: 'redmi', goodsId: '14'}])const deleteList = (index) => {     dataList.splice(index, 1)}const addItem = () => {     dataList.splice(1, 0, { name: 'SAMSUNG', goodsId: '15'})}</script><template><div>  <div class="list">      <div class="item" v-for="(item, index) in dataList">          {{ item.goodsId }}          <input type="text">          <button @click="deleteList(index)">删除</button>      </div>      <button @click="addItem">在index为1处添加一行</button>  </div></div></template>

当我们在12对应的输入框输入任意内容,然后点击12对应的删除按钮,发现内容没有变化,只有前面的goodsId发生了变化。

当点击删除后,只是改变了第一个dom元素的内容,并不是删除该元素。文本框内容是用户输入的,属于dom的默认内容不会被改变,同样添加数据也是如此。

如果key值为index那么效果和没加一样,设置了独一无二的key值就可以避开这个缺陷。因为这个key可以给Vue提示追踪每个节点的标识,最终重用和重新排序现有的元素。

组件使用v-for

<MyComponent
  v-for="(item, index) in items"
  :item="item"
  :index="index"
  :key="item.id"
/>

Vue 能够侦听响应式数组的变更方法【影响原数组】,并在它们被调用时更新视图

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

但也有一部分数组方法是不会影响原数组的,这时候需要将旧的数组替换为新数组

// `items` 是一个数组的 ref
items.value = items.value.filter((item) => item.message.match(/Foo/))

事件处理器

内联事件处理器

<button @click="count++">Add 1</button>
<p>Count is: {{ count }}</p>

方法事件处理器

const name = ref('Vue.js')

function greet(event) {
  alert(`Hello ${name.value}!`)
  // `event` 是 DOM 原生事件
  if (event) {
    //event.target.tagName访问该DOM元素
    alert(event.target.tagName)
  }
}
<!-- `greet` 是上面定义过的方法名 -->
<button @click="greet">Greet</button>

二者结合

function say(message) {
  alert(message)
}
<button @click="say('hello')">Say hello</button>
<button @click="say('bye')">Say bye</button>

function warn(message, event) {
  // 这里可以访问原生事件
  if (event) {
    event.preventDefault()
  }
  alert(message)
}
<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
  Submit
</button>

<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
  Submit
</button>

事件修饰符

支持链式写法,但需要注意顺序

  • .stop:阻止事件传播。相当于 event.stopPropagation()
  • .prevent:阻止默认行为。相当于调用 event.preventDefault()
  • .self:当事件是从事件的发起元素本身触发时才触发处理函数,如果事件是从内部元素触发的(比如通过冒泡),则不会触发处理函数。
  • .capture:添加事件侦听器时使用事件捕获模式。即事件将在从根元素向下传播到目标元素的过程中触发,而不是默认的冒泡模式。
  • .once:事件只会触发一次,触发后自动移除该事件监听器
  • .passive:指示浏览器不要等待 event 完成,可以立即执行,以防其中包含event.preventDefault

passive和prevent不能同时使用

按键修饰符

.enter,.tab,.delete,.esc,.space,.up/down/left/right

.ctrl,.alt,.shift,.meta(command 键)

.exact:精确控制触发事件所需的系统修饰符的组合

<!-- 当按下 Ctrl 时,即使同时按下 Alt 或 Shift 也会触发 -->
<button @click.ctrl="onClick">A</button>

<!-- 仅当按下 Ctrl 且未按任何其他键时才会触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- 仅当没有按下任何系统按键时触发 -->
<button @click.exact="onClick">A</button>

表单输入绑定

含义:将表单输入框的内容同步给JS中相应的变量

手动绑定

<input
  :value="text"
  @input="event => text = event.target.value">

通过v-model实现

const message = ref('')
<p>Message is: {{ message }}</p>
<input v-model="message" placeholder="edit me" />

将多个元素绑定到同一个数组或集合的值

const checkedNames = ref([])
<div>Checked names: {{ checkedNames }}</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>
------

<div>Picked: {{ picked }}</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>

修饰符

<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />
<!-- 用户输入自动转换为数字 -->
<input v-model.number="age" />
<!-- 默认自动去除用户输入内容中两端的空格 -->
<input v-model.trim="msg" />

生命周期

组件生命周期图示

作用:让开发者在特定阶段运行自己的代码

注册周期钩子:onMounted,onUpdated,onUnmoounted[常用]

调用栈必须是同步的

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

onMounted(() => {
  console.log(`the component is now mounted.`)
})
</script>

侦听器

作用:在状态变化时执行一些“副作用”,例如更改 DOM,或是根据异步操作的结果去修改另一处的状态

API:watch函数

监听对象:ref,computed,响应式对象,getter函数,多个数据源组成的数组

但监听对象不包括响应式对象的属性值,需要用一个getter 函数返回该属性

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

const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)

// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.includes('?')) {
    loading.value = true
    answer.value = 'Thinking...'
    try {
      const res = await fetch('https://yesno.wtf/api')
      answer.value = (await res.json()).answer
    } catch (error) {
      answer.value = 'Error! Could not reach the API. ' + error
    } finally {
      loading.value = false
    }
  }
})
</script>

<template>
  <p>
    Ask a yes/no question:
    <input v-model="question" :disabled="loading" />
  </p>
  <p>{{ answer }}</p>
</template>

深层监听器

隐式:直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:

显式:通过第三个参数 { deep: true }强制转换为深层监听器

即时回调侦听器

watch默认是懒执行,当数据源变化时才会执行回调。

相反场景:请求一些初始化据,然后在相关状态更改时重新请求数据

watch(
  source,
  (newValue, oldValue) => {
    // 立即执行,且当 `source` 改变时再次执行
  },
  { immediate: true }
)

一次性侦听器

回调只在源变化时触发一次,比如初始化操作,用户交互提示等

watch(
  source,
  (newValue, oldValue) => {
    // 当 `source` 变化时,仅触发一次
  },
  { once: true }
)

watchEffect()

场景:侦听器的回调使用与源完全相同的响应式状态,侦听一个嵌套数据结构中的几个属性

好处:有多个依赖项的侦听器消除手动维护依赖列表的负担

const todoId = ref(1)
const data = ref(null)
//普通写法
watch(
  todoId,
  async () => {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
    )
    data.value = await response.json()
  },
  { immediate: true }
)
//简化后的代码
watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})

watch vs watchEffect

共同点:响应式地执行有副作用的回调

watch:只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的响应式数据;仅在数据源改变时才会触发回调

watchEffect:在副作用发生期间追踪依赖,会追踪任何在回调中访问到的响应式数据

回调函数在父组件更新之后,所属DOM更新之前被调用。如果在回调函数中访问所属组件的DOM,只能获得更新前的状态。

解决方法:指明 flush: 'post' 选项,在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM

watch(source, callback, {
  flush: 'post'
})

watchEffect(callback, {
  flush: 'post'
})
watchPostEffect(() => {
  /* 在 Vue 更新后执行 */
})
//同步侦听器
watchEffect(callback, {
  flush: 'sync'
})
watchSyncEffect(() => {
  /* 在响应式数据变化时同步执行 */
})

如果用异步回调创建一个侦听器需要手工停止,避免内存泄漏

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

// 自动停止
watchEffect(() => {})

// 不会自动停止
const stopWatch2=setTimeout(() => {
  watchEffect(() => {})
}, 100)

// 手动停止
setTimeout(() => {
  stopWatch2()
}, 200) // 等待一段时间后再停止
</script>

模板引用

ref的作用:在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用

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

// 声明一个 ref 来存放该元素的引用,必须和模板里的 ref 同名
const input = ref(null)
onMounted(() => {
  input.value.focus()
})
</script>

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

列表的模板引用

<script setup>import { ref, onMounted } from 'vue'const list = ref([  'a','b'])const itemRefs = ref([])// <li>a</li>onMounted(() => console.log(itemRefs.value[0]))</script><template>  <ul>    <li v-for="item in list" ref="itemRefs">      {{ item }}    </li>  </ul></template>

函数模板引用

<template>
  <div :ref="(el) => {
  containerRef.value = el
}">
    <p>这是一个包含按钮的容器</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'
// 定义一个 ref 变量
const containerRef = ref(null)
</script>

组件

使用了 <script setup> 的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,只能在子组件中通过 defineExpose 宏显式暴露:

<!-- ParentComponent.vue -->
<template>
  <div>
    <p>父组件内容</p>
    <ChildComponent ref="child"></ChildComponent>
    <button @click="handleIncrement">从父组件增加</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const child = ref(null)

const handleIncrement = () => {
  // 在父组件中调用子组件暴露的方法
  child.value.increment()
}
</script>

<!-- ChildComponentt.vue --><template>
  <div>
    <p>子组件内容</p>
    <button @click="increment">增加</button>
  </div>
</template>

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

const count = ref(0)

const increment = () => {
  count.value++
}

// 将 increment 方法暴露给父组件
defineExpose({
  increment
})
</script>

监听事件

父组件可以通过 v-on@ 来选择性地监听子组件上抛的事件

<BlogPost
  ...
  @enlarge-text="postFontSize += 0.1"
 />

<script setup>defineProps(['title'])
//声明需要抛出的事件defineEmits(['enlarge-text'])</script><template>  <div class="blog-post">    <h4>{{ title }}</h4>
    //子组件可以通过调用内置的 $emit 方法,通过传入事件名称来抛出一个事件    <button @click="$emit('enlarge-text')">Enlarge text</button>  </div></template>

动态组件

场景:需要在2个组件之间切换

is参数值:

  • 被注册的组件名

  • 导入的组件对象

    //被注册的组件名

    //导入的组件对象

组件注册

全局注册 .component()方法

import MyComponent from './App.vue'
app.component('MyComponent', MyComponent)

局部注册

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

全局注册的缺陷:

  1. 全局注册会导致没有被使用的组件在生产打包的时候不会自动移除(tree-shaking)
  2. 过多的全局注册组件可能会使项目的结构变得混乱,导致依赖关系不够明确,降低了项目的可维护性。

props

<!-- childComponent.vue -->
<script setup>
//使用字符串数组声明 prop 
defineProps(['title'])
//使用对象的形式声明 prop,key为prop名称,value为预期类型的构造函数
defineProps({
  title: String,
  likes: Number
})
//ts
defineProps<{
  title?: string
  likes?: number
}>()
</script>

<template>
  <h4>{{ title }}</h4>
</template>

如果将一个对象的所有属性当作props传入,可以使用无参数的v-bind

<BlogPost v-bind="post" />
<BlogPost :id="post.id" :title="post.title" />
//二者等价

prop校验

defineProps({
  // 基础类型检查
  // (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
  propA: Number,
  // 多种可能的类型
  propB: [String, Number],
  // 必传,且为 String 类型
  propC: {
    type: String,
    required: true
  },
  // Number 类型的默认值
  propD: {
    type: Number,
    default: 100
  },
  // 对象类型的默认值
  propE: {
    type: Object,
    // 对象或数组的默认值
    // 必须从一个工厂函数返回。
    // 该函数接收组件所接收到的原始 prop 作为参数。
    default(rawProps) {
      return { message: 'hello' }
    }
  },
  // 自定义类型校验函数
  // 在 3.4+ 中完整的 props 作为第二个参数传入
  propF: {
    validator(value, props) {
      // The value must match one of these strings
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // 函数类型的默认值
  propG: {
    type: Function,
    // 不像对象或数组的默认,这不是一个
    // 工厂函数。这会是一个用来作为默认值的函数
    default() {
      return 'Default function'
    }
  }
})

事件

子组件中template触发事件:<button @click="$emit('someEvent',参数1,参数2,...)">click me

子组件中script中触发事件

<script setup>
const emit = defineEmits(['inFocus', 'submit'])
function buttonClick() {
  emit('submit')
}
</script>

父组件中监听事件:<MyComponent @some-event="callback" />

子组件中声明触发的事件:defineEmits(['inFocus', 'submit']),必须直接放置在 <script setup> 的顶级作用域下

TS支持

<script setup lang="ts">
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
</script>

v-model

双向绑定

<!-- Child.vue -->
<script setup>
const model = defineModel()

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

<template>
  <div>parent bound v-model is: {{ model }}</div>
</template>

<!-- Parent.vue -->
<Child v-model="count" />

defineModel返回一个ref,在父组件和当前变量之间建立双向绑定

  • 它的 .value 和父组件的 v-model 的值同步;
  • 当它被子组件变更了,会触发父组件绑定的值一起更新

多个v-model绑定

概念:在单个组件实例上创建多个 v-model 双向绑定

<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>

<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

<template>
  <input type="text" v-model="firstName" />
  <input type="text" v-model="lastName" />
</template>

插槽

作用:接收模板内容

默认内容

<template>
  <div class="alert-box">
    <strong>This is an Error for Demo Purposes</strong>
    <slot />
  </div>
</template>

如果期望没有传入slot内容的情况下有默认内容:

<button type="submit">
  <slot>
    Submit <!-- 默认内容 -->
  </slot>
</button>

具名插槽

  <BaseLayout>    <template #header>      <h1>Here might be a page title</h1>    </template>    <template #default>      <p>A paragraph for the main content.</p>      <p>And another one.</p>    </template>    <template #footer>      <p>Here's some contact info</p>    </template>  </BaseLayout>

//BaseLayout组件<template>  <div class="container">    <header>      <slot name="header"></slot>    </header>    <main>      <slot></slot>    </main>    <footer>      <slot name="footer"></slot>    </footer>  </div></template>

正常情况下,插槽内容无法访问子组件的状态

特殊场景:插槽的内容需要同时使用父组件和子组件域内的数据,通过传入props解决

//子组件
<script setup>const greetingMessage = 'hello'</script><template>  <div>    <slot :text="greetingMessage" :count="1"></slot>  </div></template>

//父组件<template>  <MyComponent v-slot="slotProps">    {{ slotProps.text }} {{ slotProps.count }}  </MyComponent></template>

依赖注入

Provide/inject 模式

父组件提供provide:

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

provide('message','hello!')
</script>

import { ref, provide } from 'vue'

const count = ref(0)
provide('key', count)

子组件作为消费者使用inject

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

const message = inject('message''默认值')
</script>
<template>  <p>    Message to grand child: {{ message }}  </p></template>

Vue-router

RouterView的插槽

作用:渲染路由组件

keepAlive组件:在多个组件间动态切换时缓存被移除的组件实例,但指的是RouterView内部的路由组件,而不是RouterView本身,所以放在RouterView内部

Transition组件: 会在一个元素或组件进入和离开 DOM 时应用动画

<router-view v-slot="{ Component }">
  <transition>
    <keep-alive>
      <component :is="Component" />
    </keep-alive>
  </transition>
</router-view>