创建一个项目
环境配置
node版本
根据官网可知,vue3需要v10以上的版本,可以通过nvm进行版本控制。
node -v
@vue/cli版本
@vue/cli是官方脚手架,通过 vue --version查看版本号,需要保证版本在v4.5.0+,可以通过以下命令,更新至最新版本:
npm install -g @vue/cli
初始化项目
1、通过 vue create vue3-basic 命令,回车,会出现如图选项:
- Default([Vue 2] babel, eslint) : vue2模板
- Default(Vue 3)([Vue 2] babel, eslint) : vue3模板
- Manualy select features:手动选择特性
2、因为我们添加typescript支持,所以选择第三项,回车:
3、选中上述内容,回车:
4、选中 3.x版本,回车
5、不选择 class-style component syntax,输入N,回车
6、是否选择 Babel 结合 typescript,Babel会自动添加 polyfills 和 转换 JXS 功能,因为不需要,所以输入 N ,回车
7、eslint配置选择默认第一项,回车
8、lint默认选择第一项,回车
9、选择配置文件是在单独的文件还是package.json文件里,选择默认在单独文件里,回车
10、是否保存以上配置在以后的安装,可自行选择,一般保存
到这儿,项目初始化完成。
执行npm run serve命令会报错,重新选择一下 eslint配置 ,安装 vue add @vue/eslint,选择 Standard模式,项目就可以启动成功。
插件文文档
推荐插件安装
1、 eslint
可检测 eslint 是否生效,如果没有生效,可在项目根目录下添加 .vscode/setting.json 文件
{
"eslint.validate": ["typescript"]
}
2、vetur
(vue2使用)
3、Volar
(vue3使用)
不要同时开启
vetur与Volar
vue3 新特性
响应性-ref与reactive
ref的妙用
带 ref 的响应式变量
假设点击一个按钮,给count递增, vue2的写法
<template>
<img alt="Vue logo" src="./assets/logo.png">
<h1>{{count}}</h1>
<button @click="increase">👍+1</button>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'App',
data () {
return {
count: 0
}
},
methods: {
increase () {
this.count++
}
}
})
</script>
vue3 引用 ref 可以不使用 data、methods也能实现同样的效果:
<template>
<img alt="Vue logo" src="./assets/logo.png">
<h1>{{count}}</h1>
<button @click="increase">👍+1</button>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'App',
setup () {
const count = ref(0)
console.log(count)
const increase = () => {
count.value++
}
return {
count,
increase
}
}
})
</script>
创建computed属性
<template>
<img alt="Vue logo" src="./assets/logo.png">
<h1>{{count}}</h1>
<h2>{{double}}</h2>
<button @click="increase">👍+1</button>
</template>
<script lang="ts">
// 引入ref
import { defineComponent, ref, computed } from 'vue'
export default defineComponent({
name: 'App',
setup () {
const count = ref(0)
console.log(count)
// 使用computed方法
const double = computed(() => {
return count.value * 2
})
const increase = () => {
count.value++
}
return {
count,
increase,
double
}
}
})
</script>
ref 解包
<template>
<img alt="Vue logo" src="./assets/logo.png">
<h1>{{count}}</h1>
<button @click="increase">👍+1</button>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'App',
setup () {
const count = ref(0)
console.log(count)
const increase = () => {
count.value++
}
return {
count,
increase,
nested: {
count,
increase
}
}
}
})
</script>
如果你不想要访问实际的对象实例,可将其用 reactive 包裹:
// 引入 reactive
import { reactive } from 'vue'
// 返回值用reactive包裹
nested: reactive({
count,
increase
})
reactive
除了 Ref可以实现响应式功能外,reactive也可以实现
<template>
<img alt="Vue logo" src="./assets/logo.png">
<h1>{{data.count}}</h1>
<h2>{{data.double}}</h2>
<button @click="data.increase">👍+1</button>
</template>
<script lang="ts">
// 引入reactive
import { computed, defineComponent, reactive } from 'vue'
interface DataProps {
count: number;
double: number;
increase: () => void;
}
export default defineComponent({
name: 'App',
setup () {
const data: DataProps = reactive({
count: 0,
increase: () => { data.count++ },
double: computed(() => data.count * 2)
})
return {
data
}
}
})
</script>
ref 和 reactive 响应式失效
因为ref 和 reactive返回的是响应式数据,如果使用解构的语法,就失去了响应的功能。(传送门)
vue3.0 针对这个问题,引入了 toRefs api.
<template>
<img alt="Vue logo" src="./assets/logo.png">
<h1>{{count}}</h1>
<h2>{{double}}</h2>
<button @click="increase">👍+1</button>
</template>
<script lang="ts">
// 引入reactive
import { computed, defineComponent, reactive, toRefs } from 'vue'
interface DataProps {
count: number;
double: number;
increase: () => void;
}
export default defineComponent({
name: 'App',
setup () {
const data: DataProps = reactive({
count: 0,
increase: () => { data.count++ },
double: computed(() => data.count * 2)
})
// 使用toRefs解决解构语法使响应式失效问题
const { count, increase, double } = toRefs(data)
return {
count,
increase,
double
}
}
})
</script>
ref 和 reactive 区别
ref函数:
语法:const xxx = ref (initValue)
- 创建一个包含响应式数据的
引用对象(reference对象,简称ref对象) - JS中操作数据:
xxx.value - 模板中读取数据:不需要
.value,直接读取xxx接受的数据类型:基本类型,引用类型
作用:把参数加工成一个响应式对象:reference对象
核心原理: - 基本类型响应式: 依赖Object.defineProperty()的get()和set()完成
- 是引用类型响应式:底层ref会借助reactive函数的proxy定义响应式
reactive函数:
语法:const xxx = reactive (源对象)
接受的数据类型:引用类型
作用:把参数加工成一个代理对象,全称为proxy对象
核心原理:基于ES6的Proxy实现,通过Reflect反射代理操作源对象,相比于reactive定义的浅层次响应式数据对象,reactive定义的是更深层次的响应式数据对象
响应式侦听 - watch
监听单个数据变化
<template>
<h2>{{greetings}}</h2>
<h2>title: {{title}}</h2>
<button style="margin: 20px;padding:20px;" @click="updateGreeting">updateGreeting</button>
</template>
<script lang="ts">
// 引入reactive
import { defineComponent,watch, ref } from 'vue'
export default defineComponent({
name: 'App',
setup () {
// watch监听数据变化
const title = ref('')
watch(greetings, (newValue, oldValue) => {
console.log(newValue)// hello
console.log(oldValue)// ''
title.value = 'update ' + greetings.value
})
return {
updateGreeting,
title
}
}
})
</script>
监听多个数据变化
监听多个数据,第一个参数用数组表示,监听函数的参数,也是对应顺序的新旧值。
<template>
<img alt="Vue logo" src="./assets/logo.png">
<h1>{{count}}</h1>
<h2>{{double}}</h2>
<h2>{{greetings}}</h2>
<h2>title: {{title}}</h2>
<button @click="increase">👍+1</button><br>
<button style="margin: 20px;padding:20px;" @click="updateGreeting">updateGreeting</button>
</template>
<script lang="ts">
// 引入reactive
import { computed, defineComponent, reactive, toRefs, watch, ref } from 'vue'
interface DataProps {
count: number;
double: number;
increase: () => void;
}
export default defineComponent({
name: 'App',
setup () {
const data: DataProps = reactive({
count: 0,
increase: () => { data.count++ },
double: computed(() => data.count * 2)
})
// 使用toRefs解决解构语法使响应式失效问题
const { count, increase, double } = toRefs(data)
const greetings = ref('')
const updateGreeting = () => {
greetings.value += 'Hello! '
}
// watch监听数据变化
const title = ref('')
watch([greetings, data], (newValue, oldValue) => {
console.log(newValue)// ['hello', Proxy] data是一个reactive对象,所以是一个Proxy对象
console.log(oldValue)// ['', Proxy]
title.value = 'update ' + greetings.value + data.count
})
return {
count,
increase,
double,
greetings,
updateGreeting,
title
}
}
})
</script>
因为data是一个 reactive对象,我们知道,reactive对象底层是通过Proxy实现的,所以,监听的结果是一个Proxy对象,如图所示:
想要正确监听到data对象下的某个属性的变化,可以使用以下方法
watch([greetings, () => data.count], (newValue, oldValue) => {
console.log(newValue) // ['hello',2]
console.log(oldValue)// ['', 0]
title.value = 'update ' + greetings.value + data.count
})
模块化的妙用
基础用法
实现一个实时打印鼠标位置的需求:
1、无模块化实现
<template>
<h2>X:{{x}}</h2>
<h2>Y:{{y}}</h2>
</template>
<script lang="ts">
// 引入reactive
import { defineComponent, ref, onMounted, onUnmounted } from 'vue'
export default defineComponent({
name: 'App',
setup () {
// 监听鼠标位置的变化
const x = ref(0)
const y = ref(0)
const updateMouse = (e: MouseEvent) => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
document.addEventListener('click', updateMouse)
})
onUnmounted(() => {
document.removeEventListener('click', updateMouse)
})
return {
x,
y
}
}
})
</script>
2、vue3支持模块化后的写法:
<template>
<h2>X:{{x}}</h2>
<h2>Y:{{y}}</h2>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
// 引入hooks
import useMousePosition from './hooks/useMousePosition'
export default defineComponent({
name: 'App',
setup () {
// 监听鼠标位置的变化
const { x, y } = useMousePosition()
return {
x,
y
}
}
})
</script>
src/hooks/useMousePostion文件
import { ref, onMounted, onUnmounted, Ref } from 'vue'
interface position {
x: Ref<number>;
y: Ref<number>;
}
function useMousePosition (): position {
const x = ref(0)
const y = ref(0)
const updateMouse = (e: MouseEvent) => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
document.addEventListener('click', updateMouse)
})
onUnmounted(() => {
document.removeEventListener('click', updateMouse)
})
return { x, y }
}
export default useMousePosition
如果未定义函数的返回值,会提示如下警告:
解决方案有两种:
- 1、定义函数的返回类型
// Ref类型需要从vue中引入
interface position {
x: Ref<number>;
y: Ref<number>;
}
- 2、在
.eslinttrc.js文件中关掉eslint的这个规则
modules.export = {
rules: {
// 每个函数都要显示声明返回值
"@typescript-eslint/explicit-module-boundary-types": "off"
}
}
模块化 + typescript泛型
src/hooks/useURLLoader文件
import axios from 'axios'
import { Ref, ref, UnwrapRef } from 'vue'
// result的类型由函数调用时决定
function useURLLoader<T> (url: string): {
result: Ref<UnwrapRef<T> | null>;
loading: Ref<boolean>;
loaded: Ref<boolean>;
error: Ref<null>;
} {
const result = ref<T | null>(null)
const loading = ref(true)
const loaded = ref(false)
const error = ref(null)
axios.get(url).then(rawData => {
loading.value = false
loaded.value = true
result.value = rawData.data
}).catch(e => {
error.value = e
loading.value = false
})
return {
result,
loading,
loaded,
error
}
}
export default useURLLoader
defineComponent
Vue3 推出defineComponent方法,没有实现任何的逻辑,只是把传入的Object对象直接返回,它的存在,是为了让传入的Object对象获得对应的类型,也就是说,它完全是为了服务typescript存在的。
组合式 API:setup
setup() 钩子是在组件中使用组合式 API 的入口,通常只在以下情况下使用:
- 需要在非单文件组件中使用组合式 API 时。
- 需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。
其他情况下,都应优先使用 <script setup> 语法。
setup()参数:
- props:该
props对象将仅包含显性声明的 prop,并且是响应式对象 - context:在
setup中无法访问this对象,context提供了this的常用的三个属性:attrs、slots、emit(对应vue2的$attrs、$slots、$emit)。
Teleport
遇到的问题
- Dialoy被包裹在其他组件之中,容易被干扰
- 样式也在其他组件中,容易变得非常混乱
Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件。传送门
teleport特性:
- 表现层跟其他子组件没有差异
- 渲染层,可以像传送门一样,将组件传送到指定的父节点下渲染
teleport属性
- to: 需要传送到哪个DOM节点下
modal.vue
<template>
<teleport to='#modal'>
<div id="center" v-if="isOpen">
<h2>
<slot>this is a modal</slot>
</h2>
<button @click="buttonClick">Close</button>
</div>
</teleport>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Modal',
props: {
isOpen: Boolean
},
emits: {
'close-modal': null
},
setup (props, context) {
const buttonClick = () => {
context.emit('close-modal')
}
return {
buttonClick
}
}
})
</script>
<style scoped>
#center {
width: 200px;
height: 200px;
border: 2px solid black;
background: white;
position: fixed;
left: 50%;
top: 50%;
margin-left: -100px;
margin-top: -100px;
}
</style>
teleport可以将这个 modal 传送到 #modal元素下,而不是 #app元素的子元素
index.html
<div id="app"></div>
<!-- 添加modal -->
<div id="modal"></div>
Suspense组件
- 解决异步请求的困境:比如,请求接口时,页面会有一个数据从无到有的过程
- Suspense是vue3推出的一个内置的特殊组件
- 如果使用
Suspense,要返回一个promise
特性:
<suspense>组件有两个插槽。它们都只接收一个直接子节点。default插槽里的节点会尽可能展示出来。- 如果
default插槽里的节点不能展示,则展示fallback插槽里的节点
asyncShow.vue
<template>
<h1>{{result}}</h1>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'AsyncShow',
setup () {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
result: 42
})
}, 3000)
})
}
})
</script>
<style scoped>
</style>
App.vue
<Suspense>
<template #default>
<async-show></async-show>
</template>
<template #fallback>Loading</template>
</Suspense>
捕获Suspense包裹的异步组件的错误,可以使用onErrorCaptured生命周期钩子函数
const error = ref(null)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onErrorCaptured((e: any) => {
error.value = e
return true
})
Provide/Inject
- Provide:数据通过props属性由上向下(由父及子)进行传递,这种特性称为
Prop Drilling
有多个子组件时,就会经过多级传递。此时,Prop Drilling就显得很繁琐。Provide/Inject的推出就是为了解决这个问题,目的就是为共享那些被认为对于一个组件树而言是”全局“的数据。
<script setup>
<script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。
ts+vue3问题总结
init
样式规范
好用的样式库:
本项目使用 bootstrap,下载
npm install bootstrap
如果使用命令npm install bootstrap@next --save,则会安装最新的线上版本,包括beta版本。
typescript支持
使用PropType指明构造函数
如果要对一个构造函数进行断言,需要使用PropType指明构造函数。详见官方文档
<template>
<div></div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
interface ColumnProps {
id: number;
title: string;
avatar: string;
description: string;
}
export default defineComponent({
name: 'ColumnList',
props: {
list: {
// 因为Array是一个构造函数,不能直接断言成一个类型,所以需要使用PropType泛型
type: Array as PropType<ColumnProps[]>,
require: true
}
},
setup () {
return {
}
}
})
</script>
vscode配置vue的Templete自动补全
vscode的setting.json文件配置
"vetur.experimental.templateInterpolationService": true,
获取html元素节点
在vue3中获取html元素的节点跟vue2有点出入,具体操作如下:
- 在
html上使用ref="xxx"标记html节点 - 在
setup中生成一个ref对象,并且该对象的名称跟步骤1的html节点名称一致 - 在
setup将步骤2的ref对象导出
备注:验证某节点是否是一个节点的子节点,使用 contains()方法。contains方法传送门
通用正则
邮箱
const emailReg = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
v-model迁移策略
当我们在定义组件上使用v-model时,vue3的语法进行了重构。
自定义组件上的 v-model 相当于传递了 modelValue prop 并接收抛出的 update:modelValue 事件。迁移策略详见文档
v-model不带参数 对于所有不带参数的v-model,请确保分别将 prop 和 event 命名更改为modelValue和update:modelValue
v-model带参数
若需要更改 model 的名称,现在我们可以为 v-model 传递一个参数,以作为组件内 model 选项的替代
<ChildComponent v-model:title="pageTitle" />
$attrs
一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 props 或 emits 定义的 attribute。常见的示例包括 class、style 和 id attribute。可以通过 $attrs property 访问那些 attribute。
- 当组件返回单个根节点时,非 prop 的 attribute 将自动添加到根节点的
attribute中。
- 如果你不希望组件的根元素继承 attribute,可以在组件的选项中设置
inheritAttrs: false。
在setup函数的第二个参数context中可以看到所有的非props的attribute
同时,可以使用v-bind绑定到任意的目标元素上。
事件监听器
在 vue2 中,Vue 实例可用于触发由事件触发器 API 通过指令式方式添加的处理函数 ($on,$off 和 $once),但vue3已经移除。在 Vue 3 中,借助这些 API 从一个组件内部监听其自身触发的事件已经不再可能了。
我们可以使用外部的库 mitt实现相同效果。
父组件 ValidateForm.vue
import mitt from 'mitt'
export const emitter = mitt()
export default defineComponent({
name: 'ValidateForm',
emits: ['form-submit'],
setup (props, context) {
const callback = (val: string) => {
console.log(val, '测试')
}
emitter.on('test', callback)
onUnmounted(() => {
emitter.off('test')
})
return {}
}
})
子组件
import { emitter } from './ValidateForm.vue'
export default defineComponent({
name: 'ValidateInput',
props: {
rules: {
type: Array as PropType<RulesProp>
},
modelValue: {
type: String
}
},
inheritAttrs: false,
setup (props, context) {
onMounted(() => {
emitter.emit('test', '12')
})
return {}
}
})
由于mitt3.0版本,对于类型定义更严格,使用时,会出现如下错误提示:
解决方案:
定义一个事件类型,并且让事件类型跟callback的参数的类型一一对应
父组件
import mitt from 'mitt'
// 定义一个事件类型
type Events = {
'test': string
}
// 实例化时,将类型作为泛型传递进去
export const emitter = mitt<Events>()
// 这里的参数类型 string 跟事件类型定义时的类型string对应
const callback = (val: string) => {
console.log(val, '测试')
}
url组成解析
ts中默认空对象报错
在typescript中,当给一个变量设置为一个空对象时,会有如下错误出现
解决办法:
const lowerObj: Record<string, unknown> = {};
vue简单工作原理
Virtual DOM:一种虚拟的,保存在内存中的数据结构,用来代表 UI 的表现,和真实 DOM 节点保持同步。Virtual DOM是由一系列的 Vnode 组成的。
Render Pipeline
- Compile, Vue 组件的 Template 会被编译成 render function,一个可以返回 Virtual DOM 树的函数。
- Mount,执行 render function,遍历虚拟DOM 树,生成真正的 DOM 节点。
- Patch,当组件中任何响应式对象(依赖)发生变化的时候,执行更新操作 。生成新的虚拟节点树,Vue 内部会遍历新的虚拟节点树,和旧的树做对比,然后执行必要的更新。
虚拟DOM 的优点
- 可以使用一种更方便的方式,供开发者操控 UI 的状态和结构,不必和真实的DOM 节点打交道。
- 更新效率更高,计算需要的最小化操作,并完成更新。
- Template 比 render function 更接近 html,更好懂,更容易修改。
- Template 更容易做静态优化,Vue 的 compiler 在编译过程中可以做很多自动的性能优化。