目录

一、环境配置

  • 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中子组件挂载全局组件

vue项目如何升级elementui版本_数据

vue3 新添加了一个默认的组件就叫 Teleport。

我们可以拿过来直接使用,它上面有一个 to 的属性,它接受一个css query selector 作为参数,这就是代表要把这个组件渲染到哪个 dom 元素中

vue项目如何升级elementui版本_App_02

<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 开发高仿知乎专栏文档站点