一、环境配置
- 1.1 安装脚手架([Vue CLI](https://cli.vuejs.org/zh/))
- 1.2 创建项目
- 1.3 VScode设置
- 二、基础语法
- 2.1 setup
- 2.2 ref
- 2.3 Reactive
- 2.4 生命周期
- 2.5 watch监听
- 2.6 模块化
- 2.7 defineComponent
- 2.8 Teleport - 瞬间移动
- 2.9 Suspense
- 2.10 全局API修改
- 三、应用
- 3.1 需求分析
- 3.2 ref
- 3.3 自定义组件v-model双向绑定
- 3.4 $attrs
- 3.5 组件传参
- 3.5.1 父传子
- 3.5.2 子传父
- 3.5.3 非父子关系传值(公共bus)
- 3.6 父组件调用子组件的方法
- 四、全家桶
- 4.1 Vue Router
- 4.1.1 Vue Router 的安装
- 4.1.2 Vue Router 的使用
- 4.1.2.1 定义路由
- 4.1.2.2 获取路由参数
- 4.1.2.3 路由跳转
- 4.2 Vuex
- 4.2.1 Vuex 的安装
- 4.2.2 Vuex 的使用
- 五、封装小组件
- 判断是内部点击还是外部点击
一、环境配置
1.1 安装脚手架(Vue CLI)
// 安装或者升级
npm install -g @vue/cli
// 保证 vue cli 版本在 4.5.0 以上
vue --version
1.2 创建项目
// 创建项目
vue create my-project
// 或者启动图形化界面创建
vue ui
根据创建提示依次选择
- Please pick a preset --- 选择 Manually select features
- Check the features needed for your project --- 多选择上 TypeScript,特别注意点空格是选择,点回车是下一步
- Choose a version of Vue.js that you want to start the project with --- 选择 3.x (Preview)
- Use class-style component syntax --- 输入 n,回车
- Use Babel alongside TypeScript --- 输入n,回车
- Pick a linter / formatter config --- 直接回车 // 选择了 ESLint + Standard config 区别就是额外添加了 Standard 代码规范。
https://standardjs.com/readme-zhcn.html
- Pick additional lint features --- 直接回车
- Where do you prefer placing config for Babel, ESLint, etc.? --- 直接回车
- Save this as a preset for future projects? --- 根据自己是否保留模板,回车
1.3 VScode设置
问:如何给 Vue 的 Template 配置 TypeScript 类型推断? 答:vetur插件中设置 vetur.experimental.templateInterpolationService为true(从0.29.0版本开始,设置以上配置即可了。)
二、基础语法
注意
- vue3中Array、Object中数据变化,能直接修改展示,不需要使用vue2中$set方法
2.1 setup
setup函数的特性
- 是使用Composition API的入口;
- 在声明周期beforeCreate事件之前被调用;
- 可以返回一个对象,这个对象的属性被合并到渲染上下文,并可以在模板中直接使用;
- 可以返回一个渲染函数,如下:
return () => h('div', [count.value, object.foo])
- 接收props对象作为第一个参数,接收来的props对象,可以通过watchEffect监视其变化。
- 接受context对象作为第二个参数,这个对象包含attrs,slots,emit三个属性。
2.2 ref
ref 是一个函数,它接受一个参数,返回的就是一个响应式对象。
<template>
<h1>{{count}}</h1>
<h1>{{double}}</h1>
<button @click="increase">+1</button>
</template>
import { ref } from "vue"
setup() {
// 我们初始化的这个 0 作为参数包裹到这个对象中去,在未来可以检测到改变并作出对应的相应。
const count = ref(0)
const double = computed(() => {
return count.value * 2
})
const increase = () => {
count.value++
}
return {
count,
increase,
double
}
}
2.3 Reactive
import { ref, computed, reactive, toRefs } from 'vue'
interface DataProps {
count: number;
double: number;
increase: () => void;
}
setup() {
const data: DataProps = reactive({
count: 0,
increase: () => { data.count++},
double: computed(() => data.count * 2)
})
return {
// 两种形式 直接data 或者 ...toRefs(data)转化
// data: 使用时用data.count、data.increase
// toRefs: 由于data为响应式对象,想要使用时直接用count、increase,需用toRefs转化下,不能直接...解构赋值,否则不起效果
...toRefs(data)
}
}
2.4 生命周期
- beforeCreate → 请使用 setup()
- created → 请使用 setup()
- beforeMount → onBeforeMount
- mounted → onMounted
- beforeUpdate → onBeforeUpdate
- updated → onUpdated
- beforeDestroy(vue3别名:beforeUnmount)→ onBeforeUnmount
- destroyed(vue3别名:unmounted)→ onUnmounted
- errorCaptured → onErrorCaptured
- renderTracked(vue3新增) → onRenderTracked
- renderTriggered(vue3新增)→ onRenderTriggered
注意: 1、因为setup是围绕beforeCreate和created生命周期挂钩运行的,所以无需显式定义它们。换句话说,将在这些挂钩中编写的任何代码都应直接在setup函数中编写。 2、onRenderTracked 、onRenderTriggered这两个事件都带有一个DebuggerEvent,它使我们能够知道是什么导致了Vue实例中的重新渲染。
import { onMounted, onUpdated, onRenderTriggered } from 'vue'
setup() {
onMounted(() => {
console.log('mounted')
})
onUpdated(() => {
console.log('updated')
})
onRenderTriggered((event) => {
console.log(event)
debugger
})
}
2.5 watch监听
import { watch } from 'vue'
setup() {
// watch 简单应用
watch(data, () => {
document.title = 'updated ' + data.count
})
// watch 的两个参数,代表新的值和旧的值
watch(refData.count, (newValue, oldValue) => {
console.log('old', oldValue)
console.log('new', newValue)
document.title = 'updated ' + data.count
})
// watch 多个值,返回的也是多个值的数组
watch([greetings, data], (newValue, oldValue) => {
console.log('old', oldValue)
console.log('new', newValue)
document.title = 'updated' + greetings.value + data.count
})
// 使用 getter 的写法 watch reactive 对象中的一项
watch([greetings, () => data.count], (newValue, oldValue) => {
console.log('old', oldValue)
console.log('new', newValue)
document.title = 'updated' + greetings.value + data.count
})
}
2.6 模块化
逻辑可以脱离组件存在,本来和组件的实现没有任何关系,我们不需要添加任何组件实现相应的功能。只有逻辑代码在里面,不需要模版。
// 将组件内逻辑抽象成可复用的函数
import { ref, onMounted, onUnmounted } from "vue"
function useMouseTracker() {
const x = ref(0)
const y = ref(0)
const updatePosition = (event: MouseEvent) => {
x.value = event.clientX
y.value = event.clientY
}
onMounted(() => {
document.addEventListener('click', updatePosition)
})
onUnmounted(() => {
document.removeEventListener('click', updatePosition)
})
return { x, y }
}
export default useMouseTracker
当返回类型多样化时,采用泛型定义,便于typescript推论出准确的数据类型
// 为函数添加泛型
function useURLLoader<T>(url: string) {
const result = ref<T | null>(null)
}
// 在应用中的使用,可以定义不同的数据类型
interface CatResult {
id: string;
url: string;
width: number;
height: number;
}
// 免费猫图片数组的 API https://api.thecatapi.com/v1/images/search?limit=1
const { result, loading, loaded } = useURLLoader<CatResult[]>('https://api.thecatapi.com/v1/images/search?limit=1')
2.7 defineComponent
在结合了 TypeScript 的情况下,传统的 Vue.extend 等定义方法无法对此类组件给出正确的参数类型推断,这就需要引入 defineComponent() 组件包装函数
- 引入 defineComponent() 以正确推断 setup() 组件的参数类型
- defineComponent 可以正确适配无 props、数组 props 等形式
- defineComponent 可以接受显式的自定义 props 接口或从属性验证对象中自动推断
- 在 tsx 中,element-ui 等全局注册的组件依然要用 kebab-case 形式
- 在 tsx 中,v-model 要用 model={{ value, callback }} 写法
- 在 tsx 中,scoped slots 要用 scopedSlots={{ foo: (scope) => () }} 写法
- defineComponent 并不适用于函数式组件,应使用 RenderContext 解决
2.8 Teleport - 瞬间移动
vue2中子组件挂载全局组件
vue3 新添加了一个默认的组件就叫 Teleport。
我们可以拿过来直接使用,它上面有一个 to 的属性,它接受一个css query selector 作为参数,这就是代表要把这个组件渲染到哪个 dom 元素中
<template>
<teleport to="#modal">
<div id="center">
<h1>this is a modal</h1>
</div>
</teleport>
</template>
2.9 Suspense
它允许我们的应用程序在等待异步组件时渲染一些后备内容,可以让我们创建一个平滑的用户体验。 值得庆幸的是,Suspense组件非常容易理解,它们甚至不需要任何额外的导入!
定义一个异步组件AsyncShow.vue,在 setup 返回一个 Promise
<template>
<h1>{{result}}</h1>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
return new Promise((resolve) => {
setTimeout(() => {
return resolve({
result: 42
})
}, 3000)
})
}
})
</script>
在引用组件处使用
<Suspense>
// 正确数据
<template #default>
<async-show />
</template>
// 预展示数据
<template #fallback>
<h1>Loading !...</h1>
</template>
</Suspense>
2.10 全局API修改
- config.productionTip 被删除
- config.ignoredElements 改名为 config.isCustomElement
- config.keyCodes 被删除
- Vue.component → app.component
- Vue.directive→ app.directive
- Vue.mixin→ app.mixin
- Vue.use→ app.use
import { createApp } from 'vue'
import App from './App.vue'
// 这个时候 app 就是一个 App 的实例,现在再设置任何的配置是在不同的 app 实例上面的。
// 不会像vue2 一样发生任何的冲突。
const app = createApp(App)
app.config.isCustomElement = tag => tag.startsWith('app-')
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)
app.config.globalProperties.customProperty = () => {}
// 当配置结束以后,我们再把 App 使用 mount 方法挂载到固定的 DOM 的节点上。
app.mount(App, '#app')
// vue2
import Vue from 'vue'
Vue.nextTick(()=>{})
const obj = Vue.observable({})
// vue3
import Vue, { nextTick, observable } from 'vue
Vue.nextTick // undefined
nextTick(() => {})
const obj = observable({})
三、应用
3.1 需求分析
一个复杂的 SPA 项目都要包括哪些知识点?
- 第一,要有数据的展示,这个是所有网站共有的特性,而且最好是有多级复杂数据的展示
- 第二,要有数据的创建,这就是表单的作用,有展示自然要有创建。在创建中,我们会发散很多问题,比如数据的验证怎样做,文件的上传如何处理,创建和编辑怎样共享单个页面等等。
- 第三,要有组件的抽象,vue 是组件的世界,组件是最重要的一环,编写组件是最基本的能力,对于一些常用的功能,我们需要高可用性和可定制性的组件,也就是说我们在整个项目中一般不会用到第三方组件,比如 element,都是从零开始,而且会循序渐进,不断抽象。甚至行成自己的一套小组件库。
- 第四,整体状态数据结构的设计和实现,SPA 一般使用状态工具管理整理状态,并且给多个路由使用,在 vue 中,我们使用 vuex,一个项目的整体数据结构的复杂程度就代表了这个能力的高低,最好是要有多层次的数据结构,相互依赖的关系,还要将数据的获取,结构设计,缓存进行一系列的考量。
- 第五,权限管理和控制,一个项目需要有用户权限的实现,不仅仅是后端,前端作为一个整体的 SPA 的项目,权限控制也尤为重要,我们需要有权限的获取,权限的持久化,权限的更新,那个路由可访问,哪个需要权限才可以访问。发送异步请求的全局 token 注入,全局拦截,全局信息提示等等和权限相关的内容。
- 第六,真实的后端API,和后端的交互是整个项目的最重要一环。一些同学在开发项目的时候会使用 mock server,但是由于后端的数据结构常常和最初的文档设计背道而驰,造成最后项目需要再次回炉修改。
3.2 ref
refs 文档地址:https://v3.vuejs.org/guide/composition-api-template-refs.html#template-refs
<template>
<div class="dropdown" ref="dropdownRef"></div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from "vue";
export default defineComponent({
setup() {
// 由于不能向vue2中使用this,这里声明一个和ref上一样的变量就可以实现了
const dropdownRef = ref<null | HTMLElement>(null);
console.log(dropdownRef.value)
return {
dropdownRef
};
}
});
</script>
3.3 自定义组件v-model双向绑定
v-model文档地址:https://v3.vuejs.org/guide/migration/v-model.html#overview
<script lang="ts">
props: {
modelValue: String //接收v-model传递过来的字段
},
// 键盘事件
const updateValue = (e: KeyboardEvent) => {
const targetValue = (e.target as HTMLInputElement).value
context.emit('update:modelValue', targetValue) // 返回上级,更新v-model的值
}
</script>
3.4 $attrs
Vue3 $attrs 文档地址: https://v3.vuejs.org/api/instance-properties.html#attrs Vue3 非 Prop 的 Attribute:https://v3.vuejs.org/guide/component-attrs.html#attribute-inheritance
// validate-input组件中
<template>
<div class="validate-input-container pb-3">
<!-- $attrs property 使用,该 property 包含了传递给一个组件的 attribute 名和 attribute 值 -->
<input v-model="inputRef.val" v-bind="$attrs">
</div>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue'
export default defineComponent({
props: {
modelValue: String,
},
// 不希望组件的根元素继承 attribut, 禁用 Attribute 继承。在使用$attrs绑定
// 注意 inheritAttrs: false 选项不会影响 style 和 class 的绑定。
inheritAttrs: false,
setup(props, context) {
const inputRef = reactive({
val: props.modelValue || '',
error: false,
message: ''
})
return {
inputRef,
}
}
})
</script>
// 组件使用方法。 组件上type、placeholder将在$attrs上绑定,不在更目录显示
<validate-input type="password" placeholder="请输入密码" v-model="passwordVal" />
3.5 组件传参
3.5.1 父传子
// 父组件
<list :list="list"></list>
// 子组件
<template>
<ul>
<li v-for="column in list" :key="column.id"> </li>
</ul>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
export interface ColumnProps {
id: number;
title: string;
avatar: string;
description: string;
}
export default defineComponent({
name: 'ColumnList',
props: {
list: {
/*
* 这里特别有一点,我们现在的 Array 是没有类型的,只是一个数组,
* 我们希望它是一个 ColomnProps 的数组,那么我们是否可以使用了类型断言直接写成 ColomnProps[],显然是不行的 ,
* 因为 Array 是一个数组的构造函数不是类型,我们可以使用 PropType 这个方法,它接受一个泛型,讲 Array 构造函数返回传入的泛型类型。
*/
type: Array as PropType<ColumnProps[]>, // 说明传递数据类型
required: true // 是否必传
}
}
})
</script>
3.5.2 子传父
<script lang="ts">
import { defineComponent} from 'vue'
export default defineComponent({
emits: ['close-message'],
setup(props, context) {
context.emit('close-message', true)
return {}
}
})
</script>
3.5.3 非父子关系传值(公共bus)
监听器 mitt插件:emit、on、off方法 mitt 文档地址:https://github.com/developit/mitt 安装 mitt
npm install mitt --save
使用mitt
import { defineComponent, onUnmounted } from 'vue'
import mitt from 'mitt'
type ValidateFunc = () => boolean
// 实例化 mitt
export const emitter = mitt()
export default defineComponent({
emits: ['form-submit'],
setup(props, context) {
let funcArr: ValidateFunc[] = []
const submitForm = () => {
// 循环执行数组 得到最后的验证结果
const result = funcArr.map(func => func()).every(result => result)
context.emit('form-submit', result)
}
// 将监听得到的验证函数都存到一个数组中
const callback = (func: ValidateFunc) => {
funcArr.push(func)
}
// 添加监听
emitter.on('form-item-created', callback)
onUnmounted(() => {
// 删除监听
emitter.off('form-item-created', callback)
funcArr = []
})
return {
submitForm
}
}
})
// 将事件发射出去,其实就是把验证函数发射出去
onMounted(() => {
emitter.emit('form-item-created', validateInput)
})
3.6 父组件调用子组件的方法
<template>
<div>
<!-- 子组件 -->
<validate-input ref="inputRef" @click="onClick" />
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import ValidateInput from '../components/ValidateInput.vue'
export default defineComponent({
components: {
ValidateInput
},
setup() {
const inputRef = ref(null)
const onClick = () => {
// 调用子组件中的方法 inputRef.value获得为 Proxy 数据
console.log(inputRef.value.validateInput())
}
return {
onClick,
inputRef
}
}
})
</script>
四、全家桶
4.1 Vue Router
4.1.1 Vue Router 的安装
vue-router-next的项目地址: https://github.com/vuejs/vue-router-next
npm install vue-router@next --save // 保证安装完毕的版本是 4.0.0 以上的
4.1.2 Vue Router 的使用
4.1.2.1 定义路由
// router.ts中
import { createRouter, createWebHistory } from 'vue-router'
import Home from './views/Home.vue'
import Login from './views/Login.vue'
// createWebHashHistory() 哈希模式
// createWebHistory() html5模式,推荐模式
const routerHistory = createWebHistory()
const router = createRouter({
history: routerHistory, // 与vue2中使用的区别
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/login',
name: 'login',
component: Login
}
]
})
// main.ts中
import router from './router'
const app = createApp(App)
app.use(router)
4.1.2.2 获取路由参数
// 定义接收路由
{
path: '/column/:id',
name: 'column',
component: ColumnDetail
}
// 获取传递过来的id
import { useRoute } from 'vue-router'
// http://localhost:8080/column/45646
import { useRoute } from 'vue-router'
// 它是一个函数,调用后可以返回对应的对象。
const route = useRoute()
// 页面传递过来的Id值
const id = route.params.id // 45646
4.1.2.3 路由跳转
第一种:可以将 to 改成不是字符串类型,而是 object 类型,这个object 应该有你要前往route 的 name ,还有对应的 params。
:to="{ name: 'column', params: { id: column.id }}"
第二种:可以在里面传递一个模版字符串,这里面把 column.id 填充进去就好。
:to="`/column/${column.id}`"
第三种:使用 useRouter 钩子函数进行跳转
import { useRouter } from 'vue-router'
setup() {
// 特别注意这个是 useRouter 而不是 useRoute,差一个字母,作用千差万别,那个是获得路由信息,这个是定义路由的一系列行为。在这里,我们可以掉用
const router = useRouter()
// router.push 方法跳转到另外一个 url,它接受的参数和 router-link 的 to 里面的参数是完全一致的,其实router link 内部和这个 router 分享的是一段代码,可谓是殊途同归了。
router.push('/login')
}
4.2 Vuex
4.2.1 Vuex 的安装
npm install vuex@next --save // 保证安装完毕的版本是 4.0.0 以上的
4.2.2 Vuex 的使用
// store.ts中
import { createStore } from 'vuex'
// 定义公用方法
const asyncAndCommit = async(url: string, mutationName: string,
commit: Commit, config: AxiosRequestConfig = { method: 'get' }, extraData?: any) => {
const { data } = await axios(url, config)
if (extraData) {
commit(mutationName, { data, extraData })
} else {
commit(mutationName, data)
}
return data
}
// 定义存储类型
export interface GlobalDataProps {
token: string;
}
const store = createStore<GlobalDataProps>({
state: {
token: localStorage.getItem('token') || '',
},
// Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,
// getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
getters: {
getColumnById: (state) => (id: string) => {
return state.columns.data[id]
},
},
// 更改 Vuex 的 store 中的状态的唯一方法
// 一条重要的原则就是要记住 mutation 必须是同步函数
mutations: {
login(state, rawData) {
const { token } = rawData.data
state.token = token
localStorage.setItem('token', token)
axios.defaults.headers.common.Authorization = `Bearer ${token}`
}
},
// Action 提交的是 mutation,而不是直接变更状态
// Action 可以包含任意异步操作
actions: {
login({ state, commit, dispatch }, payload) {
return asyncAndCommit('/user/login', 'login', commit, { method: 'post', data: payload })
},
},
})
// mian.ts中
import store from './store'
const app = createApp(App)
app.use(store)
// 页面调用
import { useStore } from 'vuex'
import { defineComponent, computed, onMounted } from 'vue'
setup() {
const store = useStore<GlobalDataProps>()
// 获取state数据,在computed计算属性中调用
const token = computed(() => store.state.token )
// 调用getters中方法,在computed计算属性中调用
const selectColumn = computed(() => store.getters.getColumnById(currentId) )
// 调用mutations中方法
store.commit('login', { data: 'xxxxx'})
// 调用actions中方法
store.dispatch('fetchColumn', currentId)
}
五、封装小组件
判断是内部点击还是外部点击
// 文件useClickOutside.ts中方法
import { ref, onMounted, onUnmounted, Ref } from 'vue'
// 接收ref对象
const useClickOutside = (elementRef: Ref<null | HTMLElement>) => {
const isClickOutside = ref(false)
const handler = (e: MouseEvent) => {
if (elementRef.value) {
if (elementRef.value.contains(e.target as HTMLElement)) {
isClickOutside.value = false
} else {
isClickOutside.value = true
}
}
}
onMounted(() => {
document.addEventListener('click', handler)
})
onUnmounted(() => {
document.removeEventListener('click', handler)
})
return isClickOutside
}
export default useClickOutside
// 方法调用
<script lang="ts">
import { defineComponent, ref, watch } from 'vue'
import useClickOutside from '../hooks/useClickOutside'
export default defineComponent({
setup() {
// 获取ref对象
const dropdownRef = ref<null | HTMLElement>(null)
// 方法调用
const isClickOutside = useClickOutside(dropdownRef)
// 监听点击事件变化
watch(isClickOutside, () => {
if (isClickOutside.value) {
// 变化后操作项
}
})
return {
dropdownRef
}
}
})
</script>
借鉴使用 Typescript + Vue3 开发高仿知乎专栏文档站点