Skip to content

Vue3

Vite 创建项目

使用 Vite 创建项目需要 node 的版本 >= 12.0.0

npm 创建命令:

shell
npm init vite vue-demo

接下来会让你选择一个需要创建的框架,这里我们选择 vue

shell
? Select a framework: » - Use arrow-keys. Return to submit.
    vanilla
>   vue
    react
    preact
    lit
    svelte

接下来是需要选择是否使用 ts,这里我不是使用就选择 vue,使用 ts 就选择 vue-ts

shell
 Select a framework: » vue
? Select a variant: » - Use arrow-keys. Return to submit.
>   vue
    vue-ts

回车选择完成之后就是要 Vite 创建了一个 Vue3 的项目

接下来进入项目目录,安装依赖后,使用 npm run dev 来启动项目

vite.config.js 配置

js
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import { resolve } from 'path'

export default defineConfig({
  base: './', // 公共路径
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, './src') // 使用 @ 访问 src 目录
    }
  },
  server: {
    host: '127.0.0.1',
    port: '2001', // 端口号
    open: true // 自动打开
  }
})

setup

setup 是在单文件组件中使用 Composition API 的编译时的语法糖,相比普通的 script 语法,它有更多的优势:

  • 更少的样板内容,更简洁的代码。
  • 能够使用纯 Typescript 声明 props 和抛出事件。
  • 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。
  • 更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)。

定义变量

setup 中可以定义函数或者变量,但是必须将这些函数或变量 return 出去,才可以正常在模板中使用。

vue
<template>
  <h1>{{ text }}</h1>

  <button @click="onclick">点击</button>
</template>

<script>
  export default {
    setup() {
      const text = 'hello vue3'

      function onclick() {
        console.log('被点击了')
      }

      return {
        text,
        onclick
      }
    }
  }
</script>

响应式变量 ref

如果想要更改某个变量的时候,那么像下面这种直接赋值的方式修改,是不能实现的:

vue
<template>
  <h1>{{ text }}</h1>

  <button @click="change">修改内容</button>
</template>

<script>
  export default {
    setup() {
      let text = 'hello vue3'

      function change() {
        text = '你好啊'
      }

      return {
        text,
        change
      }
    }
  }
</script>

如果想要使变量变成响应式,那么就需要创建响应式的变量,可以通过在 vue 来解构出一个函数来创建响应式变量

vue
<template>
  <h1>{{ text }}</h1>

  <button @click="change">修改内容</button>
</template>

<script>
  // 通过在 vue 中解构出 ref 函数
  import { ref } from 'vue'
  export default {
    setup() {
      // 使变量变成响应式变量
      let text = ref('hello vue3')

      function change() {
        // 通过变量名.value 来改变变量
        text.value = '你好啊'
      }

      return {
        text,
        change
      }
    }
  }
</script>

响应式对象

使用解构出的 reactive 可以将对象变成响应式的对象:

vue
<script>
  import { reactive } from 'vue'
  export default {
    setup() {
      const student = reactive({
        name: '小明',
        ahe: 12
      })

      return {
        student
      }
    }
  }
</script>

注:只有将对象或者变量变成响应式的才可以随意修改其值

script setup

在上面,我介绍了 vue3 的全新目录结构,整理使用组合 api,直接暴露出来一个 setup 函数,代码是这样的:

js
export default {
  setup() {
    const name = '张三'

    return {
      name
    }
  }
}

变量

通过在 setup 中写入变量或者函数,在 return 出去提供给模板使用。

但是 setup 是有个语法糖的写法,就是将 setup 当作属性直接作用到 script 标签上,如下:

vue
<template>
  {{ name }}
</template>

<script setup>
  const name = '张三'
</script>

script 标签添加 setup 属性之后,这样定义的变量 name 变量,**可以在模板中直接使用,并不需要 return 出去。**这样的代码段自然而然又变得简洁了很多。其实 script setup 就相当于在编译运行是把代码放到了 setup 函数中运行,然后把导出的变量定义到上下文中,并包含在返回的对象中。

导入组件

对于导入组件,导入之后可以直接在模板上使用,并不需要注册,也可以正常工作。

vue
<template>
  <my-button />
</template>

<script setup>
  import MyButton from './components/MyButton.vue'
</script>

获取 props

<script setup> 中必须使用 defineProps api 来声明 props

父组件

vue
<template>
  <my-button title1="这是标题1" title2="这是标题2" />
</template>

<script setup>
  import MyButton from './components/MyButton.vue'
</script>

子组件

vue
<template>
  <h1>这是我的组件-{{ title1 }}-{{ title2 }}</h1>
</template>

<script setup>
  const props = defineProps({
    title1: String,
    title2: String
  })
</script>

子组件发送 emits

<script setup> 中必须使用 defineEmits api 来声明 emits

父组件

vue
<template>
  <h1>{{ title }}</h1>
  <my-button @changeTitle="change" />
</template>

<script setup>
  import MyButton from './components/MyButton.vue'
  import { ref } from 'vue'
  const title = ref('这是父组件的内容')

  function change(val) {
    title.value = val
  }
</script>

子组件

vue
<template>
  <h2>这是我的子组件</h2>
  <button @click="onchangeTitle">点击</button>
</template>

<script setup>
  // 使用 defineEmits 来声明 emit,里面是一个数组
  // 数组的值是发送自定义事件的事件名
  const emit = defineEmits(['changeTitle'])
  function onchangeTitle() {
    emit('changeTitle', '嘿嘿嘿')
  }
</script>

缺失地方

当然,这种语法糖的写法,也是会有缺失的地方,有时候我们需要更改组件选项,比如添加 name 属性,这时候就需要再引入一个 script,在上方写入对应的export即可

vue
<script>
  export default {
    name: 'app'
  }
</script>

<script setup>
  const name = '小明'
</script>

生命周期

下面是 vue2 -> vue3 的生命周期钩子函数

beforeCreate -> setup() // 创建之前
created -> setup() // 创建完成
beforeMount -> onBeforeMount // 挂载前
mounted -> onMounted // 挂载完成
beforeUpdate -> onBeforeUpdate // 更新前
updated -> onUpdated // 更新完成
beforeDestroy -> onBeforeUnmount // 销毁前
destroyed -> onUnmounted // 销毁完成
errorCaptured -> onErrorCaptured
renderTracked -> onRenderTracked
renderTriggered -> onRenderTriggered
activated -> onActivated
deactivated -> onDeactivated

在 vue3 中,移除了 beforeCreatecreated,现在使用 setup 就可以直接优先加载了,其余钩子函数都需要通过解构引入才能进行使用。

js
import { onMounted } from 'vue'
export default {
  setup() {
    onMounted(() => {
      console.log('onMounted')
    })

    console.log('setup')
  }
}

// 输出结果:
// setup
// onMounted

那么想要通过接口获取数据就可以使用下面方式:

js
import { onMounted } from 'vue'
import axios from 'axios'
export default {
  setup() {
    onMounted(() => {
      loadData()
    })

    function loadData() {
      axios({
        method: 'GET',
        url: 'http://api.wod.xyz/Ip/outGetIpInfo?ip=57.23.66.35'
      }).then((res) => {
        console.log(res)
      })
    }
  }
}

计算属性

计算属性 computed 也是需要在 vue 中结构来引入的:

vue
<template>
  <h1>{{ num }}</h1>
</template>

<script setup>
  // 引入 computed
  import { computed } from 'vue'
  // computed 内部传入一个回调函数再赋值给变量 num
  const num = computed(() => {
    // 计算属性必须有返回值
    return 10 + 20
  })
</script>

组合 api 的优势

在之前选项 api 中,有很多钩子函数,可能在 data 中定义的很多的数据,然后 methods 中,一堆的方法,create 一堆的方法,还有各种函数中都存在很多的方法,这样就会显得非常的凌乱,不清楚那些数据和那些函数是有关系的。

但是组合 api 可以将所有的变量数据函数全部都放在 setup 一个函数中,这样其实我们可以将固定的模块抽离出一个单独的文件进行处理,然后再引入传参解构进行调用,如果逻辑很多的情况下,使用这种组合拆分的方式,你的组件里的代码就会越来越少了,每个模块单独管理方便维护。

组件绑定事件

在 Vue2 中,想要给组件绑定事件需要在子组件向父组件发送自定义事件才可以,但是在 Vue3 中,可以对组件直接进行绑定事件

子组件

vue
<template>
  <button>按钮</button>
</template>

父组件

vue
<template>
  <Btn @click="add" />
</template>

<script setup>
  import Btn from './components/demo/Btn.vue'
  function add() {
    alert('点击了')
  }
</script>

插槽

Vue3 相比 Vue2 插槽也有了一定的变化,尤其是具名插槽

普通插槽没有变化,下面来举出具名插槽的例子:

子组件

vue
<template>
  <!-- 子组件相比以往是没有变化的,具名插槽使用 name 定义插槽名称 -->
  <h1>
    <slot name="title"></slot>
  </h1>
  <p>
    <slot name="text"></slot>
  </p>
</template>

父组件

vue
<template>
  <MyCom>
    <template v-slot:title>这是标题</template>
    <template v-slot:text>这是一个段落</template>
  </MyCom>
</template>

<script setup>
  import MyCom from './components/demo/My-com.vue'
</script>

在父组件中,插槽必须统一使用 template 容器来进行存放,内部可以有其他标签,但是容器标签不能改变,在容易上使用 v-slot:插槽名 方式来绑定对应的插槽名称插入对应的内容。

注:v-slot 指令只能在 template 标签上使用!!!

获取 DOM

获取单个 DOM

vue
<template>
  <div ref="myRef">获取单个DOM元素</div>
</template>

<script setup>
  import { ref, onMounted } from 'vue'
  const myRef = ref(null)

  onMounted(() => {
    console.log(myRef)
  })
</script>

获取多个 DOM

vue
<template>
  <div :ref="setRef">
    <div>获取多个DOM元素</div>
    <input type="text" />
    <button>按钮</button>
    <p>这是一段文字</p>
  </div>
</template>

<script setup>
  const setRef = (el) => {
    console.log(el)
  }
</script>

nextTick 函数

vue
<template>
  <div class="text">获取单个DOM元素</div>
</template>

<script setup>
  import { nextTick } from 'vue'
  nextTick(() => {
    console.log(document.querySelector('.text'))
  })
</script>

组件上绑定 v-model

父组件

vue
<template>
  <!-- 组件上绑定 v-mode 对应下面一个响应式的变量 -->
  <MyInput v-model="text" />
</template>

<script setup>
  import MyInput from './components/MyInput.vue'
  import { ref } from 'vue'
  const text = ref('')
</script>

子组件

vue
<template>
  <!-- 
    type 是文本框的内容 绑定传递的参数 modelValue
    默认监听 input 事件,这里我们来调用自己定义的 myInput 函数
  -->
  <input :type="modelValue" @input="myInput" />
</template>

<script setup>
  // 在父组件上绑定了 v-model 后,相当于传递了一个 modelValue 的参数
  // modelValue 的名字的固定的,不能改变
  // 并且 prop 还会抛出 update:modelValue 事件,事件名称也是不能改变的
  const props = defineProps({
    modelValue: {
      type: String,
      default: ''
    }
  })
  const emit = defineEmits(['update:modelValue'])
  // 通过调用函数 向父组件发送文本框的内容
  function myInput(evt) {
    emit('update:modelValue', evt.target.value)
  }
</script>

<style scoped>
  input {
    width: 200px;
    height: 35px;
    border-radius: 5px;
    color: #3f536e;
    border: 1px solid #c5d9e8;
    outline: 0;
    padding: 0 10px;
    transition: border 0.35s;
    background: #fff;
    box-sizing: border-box;
  }
</style>

provide / inject

这是一个组合 api,必须两个同时使用才可以生效

provideinject 启用依赖注入。这两者只能在使用当前活动实例的 setup() 期间被调用

无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者,provide 只能通过父组件来提供给子组件,不能子组件给父组件提供,子组件通过inject 注入。

父组件

vue
<template>
  <h1>这是父组件</h1>
  <my-component></my-component>
</template>

<script setup>
  import MyComponent from './components/MyComponent.vue'
  import { provide } from 'vue'
  const say = '这是父组件提供的数据' // 需要提供的数据
  // 使用 provide 函数,第一个参数为提供数据的名称,可以自定义,第二个是需要提供的数据
  provide('AppSay', say)
</script>

子组件

vue
<template>
  <h1>这是子组件</h1>
  <h2>父组件提供的数据是:{{ res }}</h2>
</template>

<script setup>
  import { inject } from 'vue'
  // 通过 inject 函数接收父组件提供的数据,可以通过名称 AppSay 获取
  const res = inject('AppSay')
</script>

component / is

通过 component 标签配合 is 属性,可以绑定一个动态的组件进行渲染

vue
<template>
  <h1>这是父组件</h1>

  <!-- 通过计算 is 来绑定一个计算属性来渲染组件 -->
  <component :is="componentsName"></component>

  <!-- 点击按钮渲染不同的组件 -->
  <button @click="change">改变组件</button>
</template>

<script setup>
  // 引入两个组件
  import MyInput from './components/MyInput.vue'
  import MyButton from './components/MyButton.vue'
  import { computed, ref } from 'vue'
  // 通过变量控制渲染的组件
  const text = ref(true)

  // 点击按钮调用函数,改变变量控制渲染的组件
  function change() {
    text.value = !text.value
  }

  // 计算属性根据 text 的变量来返回不同的组件名称进行渲染
  const componentsName = computed(() => {
    return text.value ? 'MyInput' : 'MyButton'
  })
</script>

在组件上使用 keep-alive

我们之前曾经在一个多标签的界面中使用 is 来切换不同的组件

当在这些组件之间切换的时候,你有时会想保持这些组件的状态,比如下面引入的子组件中有一个文本框的组件 MyInput,但是当我点击按钮写换渲染的组件之后再切换回来的时候,发现之前在文本框中输入的内容没有了,但是我想在输入之后切换组件回来的时候文本框中的内容依然存在,那么就需要 keep-alive 元素将其动态组件包裹起来,那么这样的话失活的组件将会被缓存,当我切换回 MyInput 组件的时候,里面内容依然存在

html
<keep-alive>
  <component :is="componentsName"></component>
</keep-alive>

事件修饰符

.stop

下方代码中,当点击红色盒子的时候,因为父级也有一个事件,所以会导致事件冒泡,会同时触发 change1 和 change2 两个函数,那么可以通过事件对象 event.stopPropagation() 来阻止向上的冒泡:

vue
<template>
  <div @click="change1" style="background: green">
    <div @click="change2" style="background: red; width: 200px">123</div>
  </div>
</template>

<script setup>
  function change1() {
    console.log('change1')
  }
  function change2(event) {
    event.stopPropagation()
    console.log('change2')
  }
</script>

但是在 vue 中又加入了 dom 操作,显然是不太友好的,那么就可以通过事件修饰符 .stop 来阻止向上的事件冒泡:

vue
<template>
  <div @click="change1" style="background: green">
    <div @click.stop="change2" style="background: red; width: 200px">123</div>
  </div>
</template>

<script setup>
  function change1() {
    console.log('change1')
  }
  function change2() {
    console.log('change2')
  }
</script>

.capture

还有一种情况是,想要在捕获阶段就执行函数,因为 change1 在外面,我希望先执行 change1,然后再执行 change2,通过事件修饰符 .capture 可以实现效果:

vue
<template>
  <div @click.capture="change1" style="background: green">
    <div @click.capture="change2" style="background: red; width: 200px">
      123
    </div>
  </div>
</template>

.self

还是上面例子,当我们点击内部红色盒子的时候,其实并不是真正点击的外面绿色盒子,change1 执行是由于冒泡导致的,我希望是真正点击的时候它才会执行,通过事件修饰符 .self 可以实现效果:

vue
<template>
  <div @click.self="change1" style="background: green">
    <div @click="change2" style="background: red; width: 200px">123</div>
  </div>
</template>

.once

有时候,我希望有些事件只执行一次,之后就再也不执行,可以通过修饰符 .once 来完成效果,但这是一个小众的修饰符:

vue
<template>
  <div @click="change1" style="background: green">
    <div @click.once="change2" style="background: red; width: 200px">123</div>
  </div>
</template

.prevent

用 .prevent 可以阻止一些元素的默认行为:

vue
<template>
  <a href="https://baidu.com" @click.prevent="link">百度</a>
</template>

.passive

因为有些元素会自带默认行为,那么有默认行为的事件,js 都会首先检测一下是否有阻止 默认行为,比如 <a href="''></a> ,其实像这种的还好,因为我们不会经常的去使用,那么如果有类似滚动的元素,那么每滚动一下,js 都要进行判断是否有阻止默认行为,这样就会有一定的性能损伤,所以页面可能会有一定的性能降低,尤其在移动端,可能不会显得那么的顺滑,如果所有的默认行为都不阻止,js 也有不用判断了,那么就可以用到了 .passive 修饰符

passive 修饰符的含义就是:默认行为在 js 内部不需要判断了,我不阻止你。所以可以省去这一段的判断环节,例如下方滚动案例,每当滚动的时候,js 都需要检测一下是否阻止了默认行为,所以需要加入事件修饰符:

vue
<template>
  <div id="box" @scroll.passive="onScroll">
    <h1 v-for="(s, i) in 30" :key="i">{{ s }}</h1>
  </div>
</template>

<script setup>
  function onScroll() {
    console.log('11')
  }
</script>

<style scoped>
  #box {
    height: 300px;
    overflow-y: auto;
    border: 5px solid black;
  }
</style>

表单修饰符

.lazy

在以下场景中,数据进行双向绑定,在文本框中输入内容之后,会同步到 p 标签中,但是有些时候逻辑过于多的时候,或者请求后端,这样同步更新数据会有一定的性能损耗,所以给双向绑定的 v-model 属性添加 .lazy 修饰符可以在输入途中不会同步,当文本框失去焦点之后再进行同步数据:

vue
<template>
  <input type="text" v-model.lazy="text" />
  <p>{{ text }}</p>
</template>

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

.number

下方例子中,双向绑定的值希望 +100 作为结果显示,但是正常状态下,会将文本框内输入的变成字符串来解析,那么就变成了字符串拼接:

vue
<input type="text" v-model="text" />
<p>{{ text + 100 }}</p>

但是如果希望变成的是加和的状态,就需要修饰符 .number 来解决,这样就可以将文本框内输入的以数字形式解析出来并加和

vue
<input type="text" v-model.number="text" />
<p>{{ text + 100 }}</p>

如果你在文本框内输入了汉字或者英文字母之后,就算你加入了 .number 修饰符,系统也不会正常工作,要确保你输入的是纯数字,才可以正常工作!

.trim

在输入标题的时候,可能会将标题等发送至后端存入数据库中,但是并不希望开头和结尾插入空格,因为当用户每输入一个空格,虽然是看不到的,但是仍然会占用数据库的存储空间,这时就可以使用 .trim 来清除文本开头和结尾的空格,无论输入多少空格,都默认会被清除

vue
<input type="text" v-model.trim="title" />
<p>{{ title }}</p>

注意:仅仅可以清楚段落开头和结尾的空格,作用在中间部分的空格会被保留!

键盘鼠标事件

当我们按下键盘的时候就会触发事件:

vue
<template>
  <input type="text" @keyup="key" />
</template>

<script setup>
  function key(event) {
    console.log(event)
  }
</script>

那么在接受到的事件对象中我们可以看到里面有一个key的属性,就是我们当前按下的键位

也可以通过事件修饰符来指定按下哪个键位触发函数,比如下方按下 b 触发函数:

vue
<template>
  <input type="text" @keyup.b="key" />
</template>

<script setup>
  function key(event) {
    console.log(event.key)
  }
</script>

也可以结合使用快捷键,例如下方 ctrl + b 触发事件:

vue
<template>
  <input type="text" @keyup.ctrl.b="key" />
</template>

鼠标左中右事件定义方式:

vue
<template>
  <button @click.left="change1">按钮1</button>
  <button @click.middle="change2">按钮2</button>
  <button @click.right="change3">按钮3</button>
</template>

<script setup>
  const change1 = () => console.log('change1')
  const change2 = () => console.log('change2')
  const change3 = () => console.log('change3')
</script>

在 style 中使用 v-bind

vue3 中可以在 style 中使用 v-bind 来绑定 js 中的变量,比如下面例子中,一个累加器来渲染不同的颜色

vue
<template>
  <h1 @click="add">切换颜色</h1>
</template>

<script setup>
  import { ref } from 'vue'
  const text = ref('1')
  const color = ref('red')
  const add = () => {
    text.value++
    text.value % 2 === 0 ? (color.value = 'red') : (color.value = 'blue')
  }
</script>

<style scoped>
  h1 {
    color: v-bind(color);
  }
</style>

watch

累加器每次数值进行改变的时候执行 watch 函数,下面是 vue3 中 watch 的写法:

vue
<template>
  <button @click="add">{{ text }}</button>
</template>

<script setup>
  import { ref, watch } from 'vue'
  const text = ref(1)
  const add = () => text.value++
  watch(
    () => text.value,
    (newText, oldText) => {
      // newText 是新值,oldText 是老值
      console.log(newText, oldText)
    }
  )
</script>

vue3 的特性

  1. RFC

在 Github 接受整个社区的需求和请求来 讨论整个 Vue 的发展方向

  1. 新的相应式系统

新的相应式系统是基于 ES6 的 proxy 来设计的

  1. Composition API 和 <script setup>

script setup 可以更快的帮我们编写 Composition

  1. TypeScript 重构

调试阅读源码更加方法,每个数据都有数据类型,每个函数知道参数和返回对象是什么

  1. Vite

速度非常快,的工程化工具

Src 引入

如果你倾向于将 *.vue 组件拆分为多个文件,可以使用 src attribute 来引入外部的文件作为语言块

下面列举出三个文件,分别是 index.html index.js index.css

html

html
<h1>{{ text }}</h1>
<button @click="change">点击</button>

<ul>
  <li v-for="(item, index) in arr" :key="index">{{ item }}</li>
</ul>

js

js
import { ref } from 'vue'
export default {
  setup() {
    const text = ref('这是首页app组件')
    const arr = [1, 2, 3, 4, 5]
    function change() {
      alert('点击了')
    }
    return {
      change,
      text,
      arr
    }
  }
}

css

css
h1 {
  color: red;
}

这样分别写好之后,然后引入到组件中,那么这时候我们的组件的样子就是:

vue
<template src="./app/index.html"></template>

<script src="./app/index.js"></script>

<style scoped src="./app/index.css"></style>

可以试一下效果依然可以实现