使用vite工具搭建项目
使用技术:vite+vue3+typescript
项目地址:https://github.com/shenshuai89/vite-vue3-ts
项目搭建
官方推荐两种方式安装
- vite
npm init vite-app vite-vue3-ts # OR yarn create vite-app vite-vue3-ts
cli
npm install -g @vue/cli # OR yarn global add @vue/cli<br />vue create vite-vue3-ts<br />`vite` 是一个由原生`ESM`驱动的Web开发构建工具,打开 `vite` 依赖的 `package.json` 可以发现在 `devDependencies` 开发依赖里面已经引入了`TypeScript` ,甚至还有 `vuex` , `vue-router` ,`less` , `sass` 这些本地开发经常需要用到的工具。`vite` 轻量,开箱即用的特点,满足了大部分开发场景的需求,作为快速启动本地 `Vue` 项目来说,这是一个非常完美的工具。
安装依赖
使用yarn启动项目
引入typescript
安装
yarn add typescript -D
初始化
tsconfig.jsonnpx tsc --init
将
main.js修改为main.ts,同时将index.html里面的引用也修改为main.ts,
然后在script 里添加 lang=”ts”由于ts无法识别vue文件,还需要配置一个文件
在项目根目录添加
shim.d.ts文件declare module "*.vue" {import { Component } from "vue";const component: Component;export default component;}
Composition API风格
compositionAPI目的是把功能相同的代码放在一起维护。维护一个功能点时,只用修改当前的功能逻辑。compositionAPI通过setup选项组织代码。
export default {setup(props, context) {}};
新语法的范式:
export default {setup() {const { networkState } = useNetworkState();const { user } = userDeatil();const { list } = tableData();return {networkState,user,list,};},};function useNetworkState() {}function userDeatil() {}function tableData() {}
在 vue3 的 Composition API 代码风格中,比较有代表性的api就是 ref 和 reactive
ref和computed
- ref将基本类型转为响应式数据,取数据时获取value
computed计算属性数据,缓存的数据
import { computed, ref } from "vue";export default {setup() {const msg = ref("北鸟南游");const age = ref(5);// computed 计算属性的使用const double = computed(() => {return age.value * 2;});const add = () => {age.value += 1;};return { msg, age, add, double };},};
reactive和toRefs
reactive将对象转为响应式数据
toRefs将响应式对象变成普通对象
import { computed, reactive, ref, toRefs } from "vue";interface Person {name: string;age: number;}export default {setup() {// 使用reactive将对象转为响应式数据const state: Person = reactive({name: "北鸟南游",age: 5,double: computed(() => {return state.age * 2;}),});const add = () => {state.age += 1;};//toRefs将响应式对象变成普通对象return { ...toRefs(state), add };},};
props和context父子组件 属性传递
props是父组件传入的数据,context是一个上下文对象,是从2.x暴露出来的一些属性:attrs、slots、emit。
注:props的数据也需要通过toRefs解构,否则响应式数据会失效。
代码文件views/person3.vue
- views/person4.vue
- components/PersonInfo.vue
<!-- 父组件 --><template><div id="person"><PersonInfo :info="msg"></PersonInfo><div>{{ name }}年龄为{{ age }}</div><button @click="add">+</button><div>年龄的2倍{{ double }}</div></div></template><script lang="ts">import { computed, reactive, ref, toRefs } from "vue";import PersonInfo from "../components/PersonInfo.vue";interface Person {name: string;age: number;}export default {components: {PersonInfo,},setup() {// 使用reactive将对象转为响应式数据const state: Person = reactive({name: "北鸟南游",age: 5,msg: "vue3前端开发",double: computed(() => {return state.age * 2;}),});const add = () => {state.age += 1;};//toRefs将响应式对象变成普通对象return { ...toRefs(state), add };},};</script>
<!-- 子组件 --><template><div>描述:{{ data }}</div><button @click="emitName">开心学习!</button></template><script>import { ref } from "vue";export default {props: {info: String,},setup(props, context) {//利用 setup 的第一个参数获取 props 使用// console.log(props);const data = ref(props.info);// 子组件向父组件发送事件const emitName = () => {context.emit("learn", "学习vue3");};return { data, emitName };},};</script>
watch和watchEffect监听
watch数据变化,和vue2中的watch类似
watch默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调,不能立即执行。
watch监听state的age数据变化。
watch监听ref// 第一个参数:监听的数据源// 第二个是回调函数,当监听的数据发生变化,执行回调函数,并且可以获取到新值和旧值。watch(() => state.age,(newVal, oldVal, clean) => {console.log(state.name + "去年:" + oldVal + "岁,今年:" + newVal + "岁。");// clean处理重复性的watch监听事件clean(() => {console.log("clean");});});
不指定数据源
// watchEffect不指定数据源,会监听回调函数中所有的值的改变就会执行。const age = ref(18)watchEffect(()=>{console.log(age.value)})
指定数据源
const age = ref(18)// watch监听的数据源,如果是ref定义的数据,可以直接设置为第一个参数。如果是reactive定义的数据,需要传回调函数watch(age,()=> {// ref对象监听的是对象的valueconsole.log(age.value)})//使用reactive定义ageconst data = reactive({age: 5})watch(()=>data.age, (newVal, oldVal)=> {// ref对象监听的是对象的valueconsole.log(newVal, oldVal)})
watch监听reactive
const state: Person = reactive({name: "北鸟南游",age: 5,msg: "vue3前端开发",double: computed(() => {return state.age * 2;}),});
不指定数据源
watchEffect(()=>{console.log(state.age)})
指定数据源
watch(()=>state.age,()=>{console.log(state.age)})
3.监听多个数据源,数组中参数任意一个变化,都会监听到值变化。
watch([() => state.age, () => state.name],([newName, newAge], [oldName, oldAge]) => {console.log(newName);console.log(newAge);console.log(oldName);console.log(oldAge);})
watchEffect的使用
watchEffect立即执行回调函数,不用指定数据源,响应式追踪其依赖,并在其依赖变更时重新运行该函数。
watchEffect自动收集回调函数中包含的参数值的变化,并进行监听。 ```typescript
<a name="u9dwJ"></a>#### watchEffect与watch的区别:1、watch 是需要传入侦听的数据源,而 watchEffect 是**自动收集数据源作为依赖**。<br />2、watch 可以访问侦听状态变化前后的值,而 watchEffect 只能获取到变化后的值,无法获取oldValue。<br />3、watch 是属性改变的时候执行,而 watchEffect 是默认会立即执行一次,然后属性改变也会执行。<a name="vCjYp"></a>### provide、inject依赖注入数据代码参考文件- components/Son.vue- components/Grandson.vue- App.vueApp.vue```javascriptimport Son from "./components/Son.vue"export default {setup() {provide('themecolor', 'orange')}};
Son.vue
<template><div><h3 :style="{ color: color }">son 组件</h3><Grandson></Grandson></div></template><script lang="ts">import { inject } from "vue";import Grandson from "./Grandson.vue";export default {components: {Grandson: Grandson,},setup() {const color = inject("themecolor");return {color,};},};</script>
Grandson.vue
<template><div><h5 :style="{ color: color }">grandson 组件</h5></div></template><script lang="ts">import { inject } from "vue";export default {name: "grandson",setup() {const color = inject("themecolor");return {color,};},};</script>
provide和inject的使用对有层级关系的组件,可以跨组件进行数据传递
生命周期LifeCycle Hooks
新版的生命周期函数,可以按需导入到组件中,且只能在 setup() 函数中使用, 但是也可以在setup 外定义, 在 setup 中使用
<script lang="ts">import { defineComponent, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onErrorCaptured, onMounted, onUnmounted, onUpdated } from 'vue';export default defineComponent({setup(props, context) {onBeforeMount(()=> {console.log('beformounted!')})onMounted(() => {console.log('mounted!')})onBeforeUpdate(()=> {console.log('beforupdated!')})onUpdated(() => {console.log('updated!')})onBeforeUnmount(()=> {console.log('beforunmounted!')})onUnmounted(() => {console.log('unmounted!')})onErrorCaptured(()=> {console.log('errorCaptured!')})return {}}});</script>
fragment组件
在vue2中,组件模版必须有根节点
<template><div><span></span><span></span></div></template>
在Vue3中我们可以直接不需要根节点:
<template><span>hello</span><span>world</span></template>
teleport组件
可以将插槽中的元素或者组件传送到页面指定的其他位置。Teleport一个常见的使用场景,在一些嵌套比较深的组件来转移模态框的位置。虽然在逻辑上模态框是属于该组件的,但是在样式和DOM结构上,嵌套层级后较深后不利于进行维护(z-index等问题)
<template><button @click="showDialog = true">打开模态框</button><teleport to="body"><div class="modal" v-if="showDialog" style="position: fixed">我是一个模态框<button @click="showDialog = false">关闭</button><child-component :msg="msg"></child-component></div></teleport></template><script>export default {data() {return {showDialog: false,msg: "hello"};},};</script>
以上代码的teleport中的modal被传送到body标签底部。
虽然在不同的地方进行渲染,但是Teleport中的元素和组件还是属于父组件的逻辑子组件,还是可以和父组件进行数据通信。
teleport接收两个参数:to【string类型】和disabled【boolean类型】
- to:必须是有效的查询选择器或 HTMLElement,可以id或者class选择器等
- disabled:如果是true表示禁用teleport的功能,其插槽内容将不会移动到任何位置,默认false不禁用
Suspense组件
Vue3 新增 React.lazy 类似功能的 defineAsyncComponent 函数,处理动态引入(的组件)。defineAsyncComponent可以接受返回承诺的工厂函数。当您从服务器检索到组件定义时,应该调用Promise的解析回调。您还可以调用reject(reason)来指示加载已经失败
动态加载组件
import { defineAsyncComponent } from 'vue'const AsyncComp = defineAsyncComponent(() =>import('./components/AsyncComponent.vue'))app.component('async-component', AsyncComp)
suspense的应用
<template><Suspense><template #default><my-component /></template><template #fallback>Loading ...</template></Suspense></template><script lang='ts'>import { defineComponent, defineAsyncComponent } from "vue";const MyComponent = defineAsyncComponent(() => import('./Component'));export default defineComponent({components: {MyComponent},setup() {return {}}})</script>
非兼容的功能v2和v3区别
data、mixin、filter(v3已经删除)
在vue2中data可以定义object或function,如果定义成object,则数据会相互影响,因为object是引用类型。
在vue3中data只接受function,通过function返回对象;
mixin使用发生了改变,当mixin和data合并时,会执行浅拷贝合并。
const Mixin = {data() {return {user: {name: 'Jack',id: 1,address: {prov: 2,city: 3,},}}}}const Component = {mixins: [Mixin],data() {return {user: {id: 2,address: {prov: 4,},}}}}// vue2结果:{id: 2,name: 'Jack',address: {prov: 4,city: 3}}// vue3结果:user: {id: 2,address: {prov: 4,},}
vue2.x会进行深拷贝,对data中的数据向下深入合并拷贝;而vue3只进行浅层拷贝,对data中数据发现已存在就不合并拷贝。
v-model
在vue2中v-model相当于绑定value属性和input事件,本质是一个语法糖;
<child-component v-model="msg"></child-component><!-- 相当于 --><child-component :value="msg" @input="msg=$event"></child-component>
但是在特殊情况下,需要对多值进行双向绑定,其他值的显示就需要使用回调函数来改变。
<child-componentv-model="msg":msg1="msg1"@change1="msg1=$event":msg2="msg2"@change2="msg2=$event"></child-component>
在vue2.3+以后新增.sync修饰符,本质也是语法糖,是在组件上绑定@update:propName回调,语法更简洁。
<child-component:msg1.sync="msg1":msg2.sync="msg2"></child-component><!-- 相当于 --><child-component:msg1="msg1"@update:msg1="msg1=$event":msg2="msg2"@update:msg2="msg2=$event"></child-component>
vue3将v-model和.sync修饰符进行整合,弃用sync属性,使用多个value绑定值直接用多个v-model。单个v-model将v-model默认传的prop名称由value改成modelValue:
单个v-model
<child-componentv-model="msg"></child-component><!-- 相当于 --><child-component:modelValue="msg"@update:modelValue="msg = $event"></child-component>
多个v-model
v-model传递多个值,可以将一个argument传递给v-model
<child-componentv-model.msg1="msg1"v-model.msg2="msg2"></child-component><!-- 相当于 --><child-component:msg1="msg1"@update:msg1="msg1=$event":msg2="msg2"@update:msg2="msg2=$event"></child-component>
v-for和key
在vue2中,v-for不能绑定在template上,v-for每次循环都需要给每个子节点一个唯一的key
<template v-for="item in list"><div :key="item.id">...</div><span :key="item.id">...</span></template>
vue3中key值可以被放到template标签,就不能为每个子节点设置key
<template v-for="item in list" :key="item.id"><div>...</div><span>...</span></template>
v-bind
在vue2中,v-bind绑定一个对象时,对象中的值会被属性值覆盖
<div id="red" v-bind="{ id: 'blue' }"></div><div v-bind="{ id: 'blue' }" id="red"></div><!-- 最后结果都相同 --><div id="red"></div>
vue3中,如果v-bind绑定的对象里包含属性值,同时又定义属性值,那么会使用后定义的值为准。
<!-- template --><div id="red" v-bind="{ id: 'blue' }"></div><!-- result --><div id="blue"></div><!-- template --><div v-bind="{ id: 'blue' }" id="red"></div><!-- result --><div id="red"></div>
v-for中的ref属性
vue2中在v-for中使用ref属性,通过this.$refs会得到一个数组
<template<div v-for="item in list" :ref="setItemRef"></div></template><script>export default {data(){list: [1, 2]},mounted () {// [div, div]console.log(this.$refs.setItemRef)}}</script>
这样的数组可能不是想要的结果,因此vue3不再自动创建数组,而是将ref变为函数,该函数默认传入该节点
<template<div v-for="item in 3" :ref="setItemRef"></div></template><script>import { reactive, onUpdated } from 'vue'export default {setup() {let itemRefs = reactive([])const setItemRef = el => {itemRefs.push(el)}onUpdated(() => {console.log(itemRefs)})return {itemRefs,setItemRef}}}</script>
v-if和v-for的优先级
vue2中,v-for和v-if同时使用v-for有更高的优先级,因此vue2做性能优化的一点就是不能把v-for和v-if放在同一个元素上。
vue3中,v-if比v-for有更高优先级。下面代码在vue2中可以允许,但是在vue3中,没有item变量时会报错
<template><div v-for="item in list" v-if="item % 2 === 0" :key="item">{{ item }}</div></template><script>export default {data() {return {list: [1, 2, 3, 4, 5],};},};</script>
自定义hooks
解决vue2中mixin数据来源不清晰的问题,hook可以完成逻辑的复用
创建hooks文件夹,用来存放可复用的组件
创建useCount.ts文件
import { ref, Ref, watch } from "vue";interface Range {min?: number;max?: number;}interface Result {current: Ref<number>;inc: (delta?: number) => void;dec: (delta?: number) => void;set: (value: number) => void;reset: () => void;}export default function useCount(initialVal: number, range?: Range): Result {const current = ref(initialVal);const inc = (delta?: number): void => {if (typeof delta === "number") {current.value += delta;} else {current.value += 1;}};const dec = (delta?: number): void => {if (typeof delta === "number") {current.value -= delta;} else {current.value -= 1;}};const set = (value: number): void => {current.value = value;};const reset = () => {current.value = initialVal;};watch(current, (newVal: number, oldVal: number) => {console.log(newVal);if (newVal === oldVal) return;if (range && range.min && newVal < range.min) {current.value = range.min;} else if (range && range.max && newVal > range.max) {current.value = range.max;}});return {current,inc,dec,set,reset,};}
在Count.vue中引用
import useCount from "../hooks/useCount";export default {setup() {const { current: count, inc, dec, set, reset } = useCount(2, {min: 1,max: 15,});const msg = ref("Demo useCount");return {count,inc,dec,set,reset,msg,};},};
添加router
安装vue-router
yarn add vue-router@latest
配置vue-router
在项目src目录下面新建router目录,然后添加index.ts文件
import {createRouter, createWebHashHistory} from 'vue-router'// 在 Vue-router新版本中,需要使用createRouter来创建路由export default createRouter({// 指定路由的模式,此处使用的是hash模式history: createWebHashHistory(),// 路由地址routes: []})
vue3完整组件结构
完整的vue 3.x 完整组件模版结构包含了:组件名称、 props、components、setup(hooks、computed、watch、methods 等)
<template><div class="mine" ref="elmRefs"><span>{{name}}</span><br><span>{{count}}</span><div><button @click="handleClick">测试按钮</button></div><ul><li v-for="item in list" :key="item.id">{{item.name}}</li></ul></div></template><script lang="ts">import { computed, defineComponent, getCurrentInstance, onMounted, PropType, reactive, ref, toRefs } from 'vue';interface IState {count: 0,name: string,list: Array<object>}export default defineComponent({name: 'demo',// 父组件传子组件参数props: {name: {type: String as PropType<null | ''>,default: 'vue3.x'},list: {type: Array as PropType<object[]>,default: () => []}},components: {/// TODO 组件注册},emits: ["emits-name"], // 为了提示作用setup (props, context) {console.log(props.name)console.log(props.list)const state = reactive<IState>({name: 'vue 3.0 组件',count: 0,list: [{name: 'vue',id: 1},{name: 'vuex',id: 2}]})const a = computed(() => state.name)onMounted(() => {})function handleClick () {state.count ++// 调用父组件的方法context.emit('emits-name', state.count)}return {...toRefs(state),handleClick}}});</script>
添加vuex
安装vuex
yarn add vuex@latest
在项目src目录下面新建store目录,并添加index.ts文件
import { createStore } from "vuex";interface State {userName: string;taskList: any[];}export default createStore({state: {userName: "北鸟南游",taskList: [],},getters: {totalList(state: State) {return state.taskList.length;},completeList(state: State) {return state.taskList.filter((list) => {return list.isfinished == true;}).length;},},mutations: {createTask(state: State, newTask: string) {state.taskList.push(newTask);},deleteTask(state: State, index: number) {state.taskList.splice(index, 1);},updateStatus(state: State, payload: any) {const { index, status } = payload;state.taskList[index].isfinished = status;},},});
在main.ts中引入router和vuex
import router from "./router/index";import vuex from "./store/index";createApp(App).use(router).use(vuex).mount("#app");
项目todolist
集成路由和vuex数据管理
使用了scss预处理器,安装sass,yarn add sass -D
app.vue
<div id="nav"><router-link to="/">count</router-link> |<router-link to="/todolist">todolist</router-link></div><router-view />
router/index.ts
import { createRouter, createWebHashHistory } from "vue-router";// 在 Vue-router新版本中,需要使用createRouter来创建路由export default createRouter({// 指定路由的模式,此处使用的是hash模式history: createWebHashHistory(),// 路由地址routes: [{path: "/",// 必须添加.vue后缀component: () => import("../views/Count.vue"),},{path: "/todolist",name: "todolist",component: () => import("../views/TodoList.vue"),},],});
store/index.ts
import { createStore } from "vuex";interface State {userName: string;taskList: any[];}export default createStore({state: {userName: "北鸟南游",taskList: [],},getters: {totalList(state: State) {return state.taskList.length;},completeList(state: State) {return state.taskList.filter((list) => {return list.isfinished == true;}).length;},},mutations: {createTask(state: State, newTask: string) {state.taskList.push(newTask);},deleteTask(state: State, index: number) {state.taskList.splice(index, 1);},updateStatus(state: State, payload: any) {const { index, status } = payload;state.taskList[index].isfinished = status;},},});
