vue3中组合API的使用

2,117 阅读14分钟

vue3文档v3.cn.vuejs.org

Composition API字面意思是组合API,它是为了实现基于函数的逻辑复用机制而产生的。主要思想是,我们将它们定义为从新的 setup 函数返回的JavaScript变量,而不是将组件的功能(例如state、methods、computed等)定义为对象属性。

注:

vue3.0脚手架的创建(根据个人需求而定):

①创建项目:vue create 项目名

② 选择是否使用淘宝镜像(建议选,在国内比较快):

 Your connection to the default npm registry seems to be slow.
 Use https://registry.npm.taobao.org for faster installation? (Y/n) 

③选择创建项目的版本:

image-20210219003105178

④插件选项

image-20210219004415257

⑤选择vue版本

image-20210219004624017

⑥类样式语法

image-20210219004737086

⑦使用TypeScript和Babel的形式编译 JSX(我选择no)

image-20210219004959259

⑧选择路由模式

image-20210219005316054

⑨选择css预处理器的语法(建议选择 dart-sass,至于区别,自己百度)

image-20210219005415912

⑩ESLint语法配置,默认选择第一个

image-20210219010201074

⑪ESlint的特性功能

image-20210219010443334

⑫ESline配置文件时单独存放,还是直接存放在package.json文件里。这里选择放在单独的文件里。

image-20210219010628215

⑬是否保存配置

image-20210219010747130

⑭如果你同时安装了npmyarn来个包管理软件,它还会作最后一次询问,让你选择使用什么进行下载,建议npm。


vue3中的基本模板:

<!-- vue2中的html模板中必须要有一对跟标签,vue3组件模板中可以没有 -->
<template>

</template>

<script lang="ts">
// defineComponent函数,目的事定义一个组件,内部可以传入一个配置对象
import { defineComponent } from "vue";

export default defineComponent({
    name: "HelloWorld",
});
</script>

<style scoped>
</style>

注:自定义的Eslint代码检查

/*
 * @Author: 
 * @Date: 
 * @LastEditors: 
 * @LastEditTime: 
 * @FilePath: 
 * @Description:  代码检查 https://blog.csdn.net/fu983531588/article/details/109333898
 * 
 *  参数解释
    第一个参数:
    "off"或0-关闭规则
    "warn"或1-将该规则作为警告打开(不影响退出代码)
    "error"或2-将规则作为错误打开(退出代码将为1)
 */
module.exports = {
    root: true,
    env: {
        node: true
    },
    extends: [
        'plugin:vue/vue3-essential',
        'eslint:recommended',
        '@vue/typescript/recommended'
    ],
    parserOptions: {
        ecmaVersion: 2020
    },
    rules: {
        'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
        'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
        'block-scoped-var': 'off', // 块语句中使用var
        'consistent-this': ['error', 'that'], // this别名
        'default-case': 'error', // switch语句最后必须有default
        'eqeqeq': 'error', // 要求使用 === 和 !==
        'indent': ['error', 4], // 缩进风格
        'no-const-assign': 'error', // 禁止修改const声明的变量
        /**
         * always(默认):在语句末尾需要分号
         * never:不允许加分号
         */
        'semi': ['error', 'always'], // 语句强制分号结尾

    },
    overrides: [
        {
            files: [
                '**/__tests__/*.{j,t}s?(x)',
                '**/tests/unit/**/*.spec.{j,t}s?(x)'
            ],
            env: {
                jest: true
            }
        }
    ]
}

一、Vue3的响应式的原理

通过 Reflect(反射): 动态对被代理对象的相应属性进行特定的操作。

文档:

developer.mozilla.org/zh-CN/docs/…

developer.mozilla.org/zh-CN/docs/…

new Proxy(data, {
	// 拦截读取属性值
    get (target, prop) {
    	return Reflect.get(target, prop)
    },
    // 拦截设置属性值或添加新属性
    set (target, prop, value) {
    	return Reflect.set(target, prop, value)
    },
    // 拦截删除属性
    deleteProperty (target, prop) {
    	return Reflect.deleteProperty(target, prop)
    }
})

proxy.name = 'tom'   
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Proxy 与 Reflect</title>
</head>
<body>
    <script>
        const user = {
            name: "John",
            age: 12
        };
        /* 
        proxyUser是代理对象, user是被代理对象
        后面所有的操作都是通过代理对象来操作被代理对象内部属性
        */
        const proxyUser = new Proxy(user, {

            get(target, prop) {
                console.log('劫持get()', prop)
                //反射对象
                return Reflect.get(target, prop)
            },

            set(target, prop, val) {
                console.log('劫持set()', prop, val)
                return Reflect.set(target, prop, val); // (2)
            },

            deleteProperty(target, prop) {
                console.log('劫持delete属性', prop)
                return Reflect.deleteProperty(target, prop)
            }
        });
        // 读取属性值
        console.log(proxyUser === user)
        console.log(proxyUser.name, proxyUser.age)
        // 设置属性值(通过代理对象修改目标对象上的某个值)
        proxyUser.name = 'bob'
        proxyUser.age = 13
        console.log(user)
        // 添加属性
        proxyUser.sex = '男'
        console.log(user)
        // 删除属性
        delete proxyUser.sex
        console.log(user)
    </script>
</body>
</html>

二、setup的使用

注:

①setup()函数是vue3中专门新增的方法,可以理解为Composition Api的入口;

②setup在组件创建之前调用的,也就是在beforeCreate、created之前创建,因此没有this;

③setup 函数在创建组件之前被调用,所以在 setup 被执行时,组件实例并没有被创建;因此在 setup 函数中,我们将 没有办法 获取到 this ;

④setup是一个函数,只在初始化时执行一次;

⑤函数如果返回对象, 对象中的属性或方法, html模板中可以直接使用;

⑥一般不要混合使用: methods中可以访问setup提供的属性和方法, 但在setup方法中不能访问data和methods;

⑦setup不能是一个async函数: 因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性数据;

⑧一般不要混合使用: methods中可以访问setup提供的属性和方法, 但在setup方法中不能访问data和methods;

⑨setup不能是一个async函数: 因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性数据。

<template>
    <div>
        {{data.count}} <br />
        {{data.double}} <br />
        <button @click="fun">增加</button>
    </div>
</template>
<script>
/**
 * 如果 定义对象,就要引入reactive,使用计算属性之前 也要引入
 */
import { computed, reactive } from "vue"
export default {
    setup() {
        const data = reactive({
            count: 1,
            // 下面这块如果是一行代码 就可以不return,加花括号 就要return
            double: computed(() => data.count * 2)
        });
        function fun() {
            data.count++
        };
        // 不管是方法,还是变量 只要在模板里面使用就要return返回(data 是属性,可以写为:data:data 简写是下面的)
        return { data, fun }
    }
}
</script>

1、正向传值

1)父组件中

<template>
    <div>
        <SubComp num="22" />
    </div>
</template>
<script>

import SubComp from "子组件路径";
export default {
    components:{
        SubComp
    },
}
</script>

2)子组件中

<template>
    <div>
        子组件
    </div>
</template>
<script>
export default {
    props:{
        num:{
            type:String
        }
    },
    // 如果要接受父组件里面传过来的,就要在setup里面书写参数,第一个属性,就是这个参数
    setup(props){
        console.log("我是setup");
        console.log(props.num);//22
    },
    
}
</script>

2、setup的参数

以子父组件为例。

1)父组件

<template>
    <div>
        <SubComp num="22" text="你好">
            <p>插槽</p>
        </SubComp>
    </div>
</template>
<script>
import SubComp from "子组件路径";
export default {
    components:{
        SubComp
    },
}
</script>

2)子组件

<template>
    <div>
        子组件
        <slot></slot>
    </div>
</template>
<script>
export default {
    props: {
        num: {
            type: String
        }
    },
    // 如果要接受父组件里面传过来的,就要在setup里面书写参数,第一个属性,就是这个参数
    /**
     * 第一个参数指的是穿过来的属性
     * 第二个参数是指上下文(可以接受 传值时,在字组件里面为定义的属性)
     * 上下文包括的属性:
       context.attrs
       context.slots
       context.parent
       context.root
       context.emit
       context.refs
     */
    /*
    setup(props, context) {//这块也可以用结构赋值 把context换成结构赋值
        console.log("我是setup");
        console.log(props.num);
        // 用attrs 也可以接收
        console.log(context.attrs.text); //你好
        //获取夫组件 插槽传递过来的数据
        console.log(context.slots.default());
    },
    */
    setup(props, { attrs, slots, emit }) {
        console.log("我是setup");
        console.log(props.num);
        // 用attrs 也可以接收
        console.log(attrs.text); //你好
        //获取夫组件 插槽传递过来的数据
        console.log(slots.default());
    },     
}
</script>

3、setup参数的具体使用见下方:

1) props参数

props参数是一个对象,里面有父组件向子组件传递的数据,并且是在子组件中使用props接收到的所有属性(包含props配置声明且传入了的所有属性的对象)。

①父组件

<template>
    <h2>父组件</h2>
    {{msg}}
    <hr />
    <Child :text="msg" />
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import Child from "组件路径/文件名.vue";
export default defineComponent({
    name: "Home",
    components: {
        Child
    },
    setup() {
        // 定义一个ref类型的数据
        const msg = ref("我是干嘛啦");

        return { msg };
    },
});
</script>

②子组件

<template>
    <h2>子组件</h2>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
    props:["text"],
    setup(props){
        console.log(props);
        console.log(props.text);
    }
})
</script>

image-20210307230735743

2)context参数

注:使用context参数的时候,也要写上props参数,负责会报错。

context参数是一个对象:里面有attrs对象(获取当前组件标签上的所有的属性的对象,但是该属性是在props中没有声明接受的搜友的尚需经的对象)、emit方法(分发事件的)、slots对象(插槽);

<template>
    <h2>子组件</h2>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
    /*
    // 一般是这样(那么要用什么,context后面就要点什么)
    setup(props,context){
        // console.log();
        console.log(context.attrs);
        console.log(context.attrs.text);
    }
    */
    // 使用结构赋值 进项简写
    setup(props, { attrs, slots, emit }) {
    }
})
</script>

①父组件

<template>
    <h2>父组件</h2>
    {{msg}}
    <hr />
    <HelloWorld :text="msg" @fun="fun" />
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import HelloWorld from "@/components/HelloWorld.vue";
export default defineComponent({
    name: "Home",
    components: {
        HelloWorld,
    },
    setup() {
        // 定义一个ref类型的数据
        const msg = ref("我是干嘛啦");
        const fun = (val:string) => {
            console.log(val);
            msg.value += val;
        }
        return { msg, fun };
    },
});
</script>

②子组件

<template>
    <h2>子组件</h2>
    <button @click="fun">更新数据</button>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
    /*
    // 一般是这样
    setup(props,context){
    }
    */
    // 使用结构赋值 进项简写
    setup(props, { attrs, slots, emit }) {
        console.log(attrs);
        console.log(attrs.text);
        // 直接传过去
        emit("fun", "哈哈哈");
        //点击之后 传过去的
        function fun() {
            emit("fun", "点击事件传的");
        }
        return { fun };
    }
})
</script>

image-20210307233256217

4、逆向传值

1)子组件

<template>
    <div>
        子组件
    </div>
</template>
<script>
export default {
    /*
    setup(props, context) {//这块也可以用结构赋值 把context换成结构赋值
        context.emit("myEmit","我是逆向传值");
    },
    */
    setup(props, { emit }) {
        emit("myEmit", "我是逆向传值");
    },
}
</script>

2)父组件

<template>
    <div>
        <SubComp @myEmit="myEmit"></SubComp>
    </div>
</template>
<script>
import SubComp from "子组件路径";
export default {
    components:{
        SubComp
    },
    setup() {
        function myEmit(val){
            console.log(val);//我是逆向传值
        }
        // 不管是方法,还是变量 只要在模板里面使用就要return返回
        return { myEmit }
    }
}
</script>

二、Composition常用API

注:会把所有api都放在 setup 中。

1、ref响应

ref()函数用来给定的值创建一个响应式的数据对象,ref()的返回值是一个对象,这个对象上只包含一个.value属性。

ref是一个函数:

①作用:定义一个响应式的数据,返回的是一个Ref对象,对象中有一个value属性,如果需要对数据进行操作,需要使用该Ref对象调用value属性的方式进行数据的操作;

②一般用来定义一个基本类型的响应式数据;

③模板中操作数据:,不需要.value;

④如果用ref对象/数组, 内部会自动将对象/数组转换为reactive的代理对象。

<template>
    <div>
        常用api 
        <hr />
        {{num}}
        <br />
        {{fun()}}
    </div>
</template>
<script>
export default {
    setup() {
        let num = 0;
        let fun = () => {
            num ++;
            console.log(num);
        }
        return { num, fun }
    }
}
</script>

image-20210215172117633

调用方式如下:

<template>
    <div>
        {{num}}
        <input type="text" v-model="num">
        <!-- 不注销他,上面就没办法输入 -->
        <!-- {{fun(2)}} -->
    </div>
</template>
<script>
// 需要引入
import { ref } from "vue";
export default {
    setup() {
        let num = ref(0);
        let fun = (val) => {
            num.value = val;
        }

        return { num, fun }
    }
}
</script>

image-20210215180951656

2、reactive是用来创建一个响应式对象

将ref响应式数据挂载到reactive中,当把ref()创建出来值直接挂载到reactive()中时,会自动把响应式数据对象的展开为原始的值,不需要通过.value就可以直接访问到:

①作用: 定义多个数据的响应式

②const proxy = reactive(obj): 接收一个普通对象然后返回该普通对象的响应式代理器对象,返回的是一个proxy 的代理对象,被代理的目标对象就是obj对象

③响应式转换是“深层的”:会影响对象内部所有嵌套的属性

④内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的

<template>
    <div>
        <hr />
        {{num}}
        <input type="text" v-model="num">
        <br />
        <!-- {{fun(2)}} -->
        <hr />
        <!-- {{user.name}} -->
        {{name}}
        {{num2}}
    </div>
</template>
<script>
// 需要引入
import { reactive, ref, toRefs, readonly, isRef } from "vue";
export default {
    setup() {
        let num = ref(0);
        let fun = (val) => {
            num.value = val;
        }

        //isRef() 判断是否是响应式的

        // 用 reactive 声明 也可以是响应式的
        let user = reactive({
            name: "啊啊啊",
            age: 18,
            num2: num, //把ref声明的放在这个 里面,就不用value值了
        })
        function fun2() {
            user.num2 = 100
        }
        fun2();
        // 通过readonly()就可以吧响应式数据变成 不响应的

        //return { num, fun, user }//这样导出一个对象,取值的时候,需要点,那么就可以使用json对象 或者 ...(扩展运算符),只不过 这样展开,数据就不是相应式的了
        //return { num, fun, ...user } //这样就不能响应式了(...)
        return { num, fun, ...toRefs(user) } //引入了这个api,使用了扩展运算符之后,他就又可以响应了
    }
}
</script>

使用的时候,建议如下:

<!-- vue2中的html模板中必须要有一对跟标签,vue3组件模板中可以没有 -->
<template>
    <button @click="updateUser">更新</button>
    <br />
    {{user}}
</template>
<script lang="ts">
// defineComponent函数,目的事定义一个组件,内部可以传入一个配置对象
import { defineComponent, reactive } from "vue";
export default defineComponent({
    name: "Home",
    setup() {
        //建议写多个对象,这样 下面好调用
        const user = reactive({
            car: {
                name: "小王",
                age: 18,
            },
            wife: {
                name: "小明",
                age: 22,
            },
        });
        // function updateUser(){}
        const updateUser = () => {
            user.car.name = "哈哈哈";
            user.wife.age++
            //删除user里面的实行
            delete user.wife.name
        }
        return { user, updateUser };
    }
});
</script>
<style scoped>
</style>

注:直接使用目标对象的方式来更新目标对象中的成员的值,是不可能的,只能使用代理对象的方式来更新数据(响应式数据)。

3、computed(计算属性)

computed()用来创建计算属性,返回值是一个ref的实例。

<template>
    计算属性:
    name: <input type="text" v-model="name"> <br/>
    age: <input type="number" v-model="age"> <br />
    sex: <input type="text" v-model="sex"> <br />
    {{name}}————{{age}}————{{sex}} <br />
    {{fullName}}
</template>
<script>
import { reactive, toRefs, computed } from 'vue';
export default {
    setup() {
        const user = reactive({
            name: "啊啊",
            age: 100,
            sex: "女"
        })
        // 计算属性(多个 的时候,同下)
        let fullName = computed(() => {
            return "名字:" + user.name + "—— 年龄:" + user.age
        })
        return { ...toRefs(user),fullName }
    }
}
</script>

4、watch()函数

watch()函数用来监视某些数据项的变化,从而触发某些特定的操作。并且他还有一个停止监听的方法。

1)watch

①与watch配置功能一致

②监视指定的一个或多个响应式数据, 一旦数据变化, 就自动执行监视回调

③默认初始时不执行回调, 但可以通过配置immediate为true, 来指定初始时立即执行第一次

④通过配置deep为true, 来指定深度监视

2)watchEffect函数

①不用直接指定要监视的数据, 回调函数中使用的哪些响应式数据就监视哪些响应式数据

②默认初始时就会执行第一次, 从而可以收集需要监视的数据

③监视数据发生变化时回调

1)ref单个变量

<template>
    监听器
    <button @click="fun1">a ———— {{a}}</button>
    <button @click="b++">b ———— {{b}}</button>
</template>
<script>
import { ref, watch, watchEffect } from 'vue'
export default {
    setup() {
        let a = ref(1);
        let b = ref(2);

        function fun1() {
            a.value = a.value + 1;
        }
        watch(() => {
            // 这里面写那个 就监听那个的变化
            console.log(a.value, b.value);
        })
        watchEffect(() => {
            console.log(a.value, b.value);
        })
        // 只监听一个的变化(具体监听 某一个),还可以确定新值和旧值,并且可以监听多个值,那么就要写成数组watch([a, b], ([newsA, newsB,], [oldsA, oldsB]) => {
        watch(a, (news, olds) => {
            console.log("监听a的变化", a.value, "新值", news, "旧值", olds);
        }, { immediate: true }) //immediate:true 加他可以默认执行一次
        return { a, b, fun1 }
    }
}
</script>

2)监听 reactive() 对象的变化

<template>
    监听器
    <button @click="a++">a ———— {{a}}</button>
    <button @click="b++">b ———— {{b}}</button>
</template>
<script>
import { reactive, ref, toRefs, watch, watchEffect } from 'vue'
export default {
    setup() {
        const user = reactive({
            a: 1,
            b: 2
        })
        // 监听对象里面某个值的变化(监听多个的时候,使用数组)
        watch(() => user.a, (news, olds) => {
            console.log(news, olds);
        })
        /*
        // 当监听某个值的时候,,就不能监听整个对象,负责会报错
        watchEffect(() => {
            console.log("我是watchEffect,默认会打印一次", a);
        })
        watch(user, () => {
            console.log("监听整个对象,任何一个变化,他都能监听到");
        }, { immediate: true })
        //这个 可以监听变化,但是 不可以具体到那个变化了
        watch(user, (news, olds) => {
            console.log(news, olds);
        })
        */
        return { ...toRefs(user) }
    }
}
</script>

注:watchEffect 立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数。

5、toRefs的使用

使用return导出一个对象的时候,那么每个对象都是一个ref函数,但是 导出对象的时候,使用的时候就需要点,很不方便,如果当时使用扩展运算符的时候,他又不会响应数据,所以这时候,就要用到toRefs函数。

问题: reactive 对象取出的所有属性值都是非响应式的。

解决: 利用 toRefs 可以将一个响应式 reactive 对象的所有原始属性转换为响应式的 ref 属性

toRefs可以吧一个响应式对象转换成普通对象,该普通对象的每个property 都是一个ref

<template>
    <p>{{name}}</p>
    <p>{{age}}</p>
    <button @click="fun">修改</button>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from "vue";
export default defineComponent({
    setup() {
        const obj = reactive({
            name: "小明",
            age: 88,
        });
        function fun() {
            obj.name = "我改名叫小王";
            console.log(obj);
        }
        // 下面这样写,数据就不实时响应了
        // return { ...obj, fun };
        // toRefs可以吧一个响应式对象转换成普通对象,该普通对象的每个property 都是一个ref
        return { ...toRefs(obj), fun };
    },
});
</script>

6、ref获取元素

让输入框自动获取焦点:

<template>
    <h2>ref 获取页面中的元素</h2>
    <input type="text" ref="inputRef" />
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from "vue";
export default defineComponent({
    setup() {
        // 需求:当页面加载完毕后,页面中的文本框可以直接 获取焦点(自动获取焦点)
        const inputRef = ref<HTMLElement | null>(null);
        onMounted(() => {
            inputRef.value && inputRef.value.focus();
        });
        return { inputRef };
    },
});
</script>

7、 shallowReactive 与 shallowRef

1)shallowReactive : 只处理了对象内最外层属性的响应式(也就是浅响应式)

2)shallowRef: 只处理了value的响应式, 不进行对象的reactive处理

3)什么时候用浅响应式呢?

①一般情况下使用ref和reactive即可

②如果有一个对象数据, 结构比较深, 但变化时只是外层属性变化 ===> shallowReactive

③如果有一个对象数据, 后面会产生新的对象来替换 ===> shallowRef

4)劫持的深度

①reactive(深度劫持)

<template>
    <p>reactive:{{m1}}</p>
    <button @click="fun">更改数据</button>
</template>
<script lang="ts">
import { defineComponent, reactive } from "vue";
export default defineComponent({
    setup() {
        const m1 = reactive({
            name: "小明",
            age: 22,
            car: {
                name: "自行车",
                color: "红色",
            },
        });
        // 更该数据
        const fun = () => {
            // 更改reactive的方式(深度劫持)
            m1.name += "啊啊啊";
            m1.car.color = "蓝色";
        }
        return { m1,fun };
    },
});
</script>

②shallowReactive(浅劫持)

<template>
    <p>shallowReactive:{{m2}}</p>
    <button @click="fun">更改数据</button>
</template>
<script lang="ts">
import { defineComponent, shallowReactive } from "vue";
export default defineComponent({
    setup() {
        const m2 = shallowReactive({
            name: "小明",
            age: 22,
            car: {
                name: "自行车",
                color: "红色",
            },
        });
        // 更该数据
        const fun = () => {
            // shallowReactive 的方式
            // m2.name += "改变";
            m2.car.color += "橘色";
        }
        return { m2,fun };
    },
});
</script>

③ref(深度劫持)

<template>
    <p>ref:{{m3}}</p>
    <button @click="fun">更改数据</button>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
    setup() {
        const m3 = ref({
            name: "小明",
            age: 22,
            car: {
                name: "自行车",
                color: "红色",
            },
        });
        // 更该数据
        const fun = () => {
            m3.value.car.name += "改变"; 
        }
        return { m3, fun };
    },
});
</script>

④shallowRef(浅劫持)

<template>
    <p>shallowRef:{{m4}}</p>
    <button @click="fun">更改数据</button>
</template>
<script lang="ts">
import { defineComponent, shallowRef } from "vue";
export default defineComponent({
    setup() {
        const m4 = shallowRef({
            name: "小明",
            age: 22,
            car: {
                name: "自行车",
                color: "红色",
            },
        });
        // 更该数据
        const fun = () => {
            m4.value.name += "改变";
            m4.value.car.name += "改变";
            console.log(m4)
        }
        return { m4 ,fun };
    },
});
</script>

8、readonly 与 shallowReadonly

在某些特定情况下, 我们可能不希望对数据进行更新的操作, 那就可以包装生成一个只读代理对象来读取数据, 而不能修改或删除 。

1)readonly是一个深度只读的

<template>
    <h3>{{state}}</h3>
    <button @click="update">更新</button>
</template>

<script lang="ts">
import { reactive, readonly, shallowReadonly } from "vue";
/*
readonly: 深度只读数据
  获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理。
  只读代理是深层的:访问的任何嵌套 property 也是只读的。
*/
export default {
    setup() {
        const state = reactive({
            a: 1,
            b: {
                c: 2,
            },
        });
        const rState2 = readonly(state);
        // 修改时,直接报错
        const update = () => {
            rState2.b.c++;
        };

        return {
            state,
            update,
        };
    },
};
</script>

image-20210314173229179

2)shallowReadonly 浅只读

<template>
    <h3>{{state}}</h3>
    <button @click="update">更新</button>
</template>

<script lang="ts">
import { reactive, readonly, shallowReadonly } from "vue";
/*
shallowReadonly: 浅只读数据
 创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 
*/
export default {
    setup() {
        const state = reactive({
            a: 1,
            b: {
                c: 2,
            },
        });

        const rState2 = shallowReadonly(state);
        // 修改时,直接报错
        const update = () => {
            rState2.a++;
            // 但是 深层次的数据,还是可以修改的
            rState2.b.c++;
        };

        return {
            state,
            update,
        };
    },
};
</script>

image-20210314173739728

9、toRaw 与 markRaw

1)toRaw

返回由 reactivereadonly 方法转换成响应式代理的普通对象。

这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发界面更新。

<template>
    <h2>{{state}}</h2>
    <button @click="fun">测试</button>
</template>

<script lang="ts">
import { markRaw, reactive, toRaw } from "vue";
export default {
    setup() {
        const state = reactive<any>({
            name: "tom",
            age: 25,
        });

        const fun = () => {
            // 把代理对象变成了普通对象,但是界面 依然没有更新
            const user = toRaw(state);
            user.age++; // 界面不会更新
        };
        return {
            state,
            fun
        };
    },
};
</script>

2)markRaw

标记一个对象,使其永远不会转换为代理。返回对象本身

<template>
    <h2>{{state}}</h2>
    <button @click="fun">测试</button>
</template>

<script lang="ts">
import { markRaw, reactive, toRaw } from "vue";
interface Info{
    name:string;
    age:number;
    likes ?: string[];
}
export default {
    setup() {
        const state = reactive<Info>({
            name: "tom",
            age: 25,
        });

        const fun = () => {
            const likes = ["a", "b"];
            // 使用markRaw标记的对象数据,从此以后 都不能再成为 代理对象了
            state.likes = markRaw(likes);//这样在界面上  就不会更改了
            setInterval(() => {
                if(state.likes){
                    state.likes[0] += "--";
                    console.log("定时器走起来");
                }
            },1000)
        };
        return {
            state,
            fun,
        };
    },
};
</script>

应用场景:

①有些值不应被设置为响应式的,例如复杂的第三方类实例或 Vue 组件对象。

②当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能。

10、toRef的使用

为源响应式对象上的某个属性创建一个 ref对象, 二者内部操作的是同一个数据值, 更新时二者是同步的;区别ref: 拷贝了一份新的数据值单独操作, 更新时相互不影响。

1)父组件

<template>
    <h2>state:{{state}}</h2>
    <h2>age:{{age}}</h2>
    <h2>money:{{money}}</h2>
    <button @click="fun">更新数据</button>
    <hr >
    <!-- 到组件里面取数据 可以省略value -->
    <api-child :age="age" />
</template>
<script lang="ts">
import { defineComponent, reactive, toRef, ref } from "vue";
import apiChild from "./component/apiChild.vue";
export default defineComponent({
    components:{
        apiChild
    },
    setup() {
        const state = reactive({
            age: 5,
            money: 100,
        });
        // 把响应式数据state对象中的某个属性age变成了ref对象了
        const age = toRef(state, "age");
        // 把响应式对象中的某个属性使用ref进行包装,变成了一个ref对象(所以他俩 不会绑定,改变下面的,state里面的值 不会改变)
        const money = ref(state.money);
        console.log(age);//ref对象
        console.log(money);//ref对象
        //跟新数据
        function fun() {
            state.age += 2;
            // age.value = 3;
        }
        return { state, age, money, fun };
    },
});
</script>

2)子组件

<template>
    <h3>子组件</h3>
    <p>age:{{age}}</p>
    <p>长度:{{length}}</p>
</template>
<script lang="ts">
import { defineComponent, computed, Ref, toRef } from "vue";
// 下面是一个hook函数
function useGetLength(age: Ref) {
    return computed(() => {
        return age.value.toString().length;
    });
}
export default defineComponent({
    props: {
        age: {
            type: Number,
            required: true, // 必须的(就是必须传这个)
        },
    },
    setup(props) {
        // 在子组件里获取传过来的长度
        const length = useGetLength(toRef(props, "age"));
        return { length };
    },
});
</script>

image-20210314182425886

11、customRef

创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制

<template>
    <input type="text" v-model="text" />
    <p>{{ text }}</p>
</template>
<script lang="ts">
import { customRef, defineComponent, ref } from "vue";
// 自定义hook防抖的函数
// value传入的数据,将来数据的类型不确定,所以,用泛型,delay防抖的间隔时间.默认是200毫秒
function useDebouncedRef<T>(value: T, delay = 200) {
    // 准备一个存储定时器的id的变量
    let time: any;
    // 它返回的是ref(track, trigger 在官方文档里面)
    return customRef((track, trigger) => {
        return {
            // 返回数据的
            get() {
                // 告诉Vue追踪数据
                track();
                return value;
            },
            // 设置数据的
            set(newValue: T) {
                // 清理定时器
                clearTimeout(time);
                // 开启定时器
                time = setTimeout(() => {
                    value = newValue;
                    // 告诉Vue更新界面
                    trigger();
                }, delay);
            },
        };
    });
}
export default defineComponent({
    setup() {
        const text = useDebouncedRef("abc", 500);
        return {
            text,
        };
    },
});
</script>

image-20210314183529452

12、判断数据是不是响应式

1)isRef: 检查一个值是否为一个 ref 对象

2)isReactive: 检查一个对象是否是由 reactive 创建的响应式代理

3)isReadonly: 检查一个对象是否是由 readonly 创建的只读代理

4)isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理

注:上面返回的是true或者false。

三、生命周期钩子

• beforeMount -> onBeforeMount

• mounted -> onMounted

• beforeUpdate -> onBeforeUpdate

• updated -> onUpdated

• beforeUnmount -> onBeforeUnmount

• unmounted -> onUnmounted

注:

①在setup中没有创建组件的钩子函数(beforeCreate、created),其余的都有;

②执行时机是相同的;

③但是写在setup中的时候,前面多加了一个on,里面还是一个回调方法;

④一定要引入;

⑤v-if显示隐藏会触发组件里面的显示隐藏。

<template>
    生命周期

</template>
<script>
//用哪个生命周期,就要在这引入
import { onMounted } from 'vue'
export default {
    setup(){
        /**
         * 跟Mounted是同时调用的,
         * 但是写在setup中的时候,前面多加了一个on
         * 里面还是一个回调方法
         * 同时一定要引入
        */ 
        onMounted(()=>{
            console.log("onMounted");
        })
    }
}
</script>

四、自定义hooks函数的使用

  • 使用Vue3的组合API封装的可复用的功能函数
  • 自定义hook的作用类似于vue2中的mixin(混入)技术
  • 自定义Hook的优势: 很清楚复用功能代码的来源, 更清楚易懂

1、案例:

1)收集用户鼠标点击的页面坐标

①在vue页面中

<template>
    <h2>自定义hooks函数</h2>
    <p>x:{{x}} ,y:{{y}}</p>
</template>
<script>
import { defineComponent } from 'vue';
//1、引入
import 文件名 from "文件的路径/文件名.ts";
export default defineComponent({
    // 需求1:用户在页面中点击页面,把点击的位置的横纵坐标收集起来并展示出来
    setup() {
        // 2、在这使用结构赋值,进行使用
        const { x, y } = 文件名();
        return { x, y }
    },
})
</script>

②创建引入的文件(这块可以是ts也可以是js)

import { onBeforeMount, onMounted, ref } from 'vue';
export default function () {
    const x = ref(-1);
    const y = ref(-1);
    /**
     * 点击的回调函数
     * @param event 他是鼠标的点击事件,所以他有一个MouseEvent类型
     */
    function clickHandler(event: MouseEvent) {
        x.value = event.pageX;
        y.value = event.pageY;
    }

    // 页面加载完毕,在进行操作,所以 就需要页面加载完毕的钩子
    onMounted(() => {
        //在这写了方法,需要在上面写回调函数
        window.addEventListener("click", clickHandler);
    })
    // 页面关闭,卸载函数
    onBeforeMount(() => {
        window.removeEventListener("click", clickHandler);
    })
    // 暴露出去,外面要用的
    return { x, y }
}

image-20210308231957330

五、在组合API中provide和inject使用

从父向后代传递数据,父向后代传递的时候,不想层层传递就用它。

图片1

1、传统方式

1)根组件或者页面

<template>
    ProvideInject的使用
    <h1>跟</h1>
    {{title}}
    <input type="text" v-model="title">
    <TwoProvide />
</template>
<script>
import TwoProvide from "./component/TwoProvide.vue";
export default {
    components:{
        TwoProvide
    },
    data() {
        return {
            title:"这是跟组件或者页面提供的数据"
        }
    },
    // 1、注入(也就是 提供数据,但是 传统方式,无法响应数据,也就后代组件里面的值,不会改变)
    provide(){
        return {
            title:this.title
        }
    }
}
</script>

2)第二层组件

<template>
    <hr />
    <p>第二层组件</p>
    <three-provide />
</template>
<script>
import ThreeProvide from "./ThreeProvide.vue";
export default {
    components:{
        ThreeProvide
    }
}
</script>

3)第三层组件

<template>
    <hr />
    <p>第三层组件</p>
    {{title}}
</template>
<script>
export default {
    // 2、使用
    inject:["title"]
}
</script>

image-20210215235628437

2、setup中的使用

1)根组件或者页面

<template>
    ProvideInject的使用
    <h1>跟</h1>
    {{title}}
    <input type="text" v-model="title">
    <TwoProvide />
</template>
<script>
import { ref, provide } from "vue";
import TwoProvide from "./component/TwoProvide.vue";
export default {
    components: {
        TwoProvide
    },
    setup() {
        let title = ref("这是跟组件或者页面提供的数据");
        // 1、注入(也就是 提供数据,但是 传统方式,无法响应数据,也就后代组件里面的值,不会改变)
        provide("title", title);

        return { title };
    }
}
</script>

2)第二层组件

<template>
    <hr />
    <p>第二层组件</p>
    <three-provide />
</template>
<script>
import ThreeProvide from "./ThreeProvide.vue";
export default {
    components:{
        ThreeProvide
    }
}
</script>

3)第三层组件

<template>
    <hr />
    <p>第三层组件</p>
    {{title}}
    <input type="text" v-model="title">
</template>
<script>
import { inject } from 'vue'
export default {
    // 2、使用(接受)
    setup() {

        let title = inject("title");
        return { title }
    }
}
</script>

image-20210216000720253

3、setup对象的方式如下

image-20210216001006786

注:其他PI参考地址vue3js.cn/vue-composi…

六、vue3中的新组件

1、Fragment(片断)

1)在Vue2中: 组件必须有一个根标签

2)在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中

3)好处: 减少标签层级, 减小内存占用

<template>
    <!-- 这个里面不需要跟标签(div) -->
</template>

2、Teleport(瞬移)

Teleport 提供了一种干净的方法, 让组件的html在父组件界面外的特定标签(很可能是body)下插入显示。

注:也就是可以通过,指定组件的显示位置。

1)父组件

<template>
    App
    <DialogBox />
    
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import DialogBox from "组件路径/DialogBox.vue";
export default defineComponent({
    components:{
        DialogBox
    }
})
</script>

2)子组件

<template>
    <button @click="fun">打开一个对话框</button>
    <!-- 下面这个 是对话框,使用teleport显示位置 -->
    <teleport to="body">
        <div v-if="modalOpen" class="modal">
            <div>
                内容
                <button @click="modalOpen = false">关闭</button>
            </div>
        </div>
    </teleport>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
    setup() {
        const modalOpen = ref(false);
        function fun() {
            modalOpen.value = true;
        }
        return { modalOpen, fun };
    },
});
</script>
<style scoped>
.modal {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background-color: rgba(0, 0, 0, 0.5);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}

.modal div {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    background-color: white;
    width: 300px;
    height: 300px;
    padding: 5px;
}
</style>

image-20210314205035486

3、Suspense(不确定的)

允许我们的应用程序在等待异步组件时渲染一些后备内容,可以让我们创建一个平滑的用户体验。

也就是在页面中要显示一个组件,但是这个组件要过一段时间进行显示,这个空白页就可以用它,可以在做骨架屏的时候使用。

1)父组件

<template>
    <h1>父组件</h1>
    <suspense>
        <!-- 
            注: v-slot:fallback 是固定的写法,v-slot的简写是 #default
         -->
        <!-- 异步组件 -->
        <template #default>
            <AsyncComponent />
        </template>
        <!-- 在异步组件还没加载完成时,显示的内容 -->
        <template v-slot:fallback>
            <h3>Loading。。。</h3>
        </template>
    </suspense>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from "vue";
// 组件的引入 分为动态引入和静态引入
// 1、vue2中的动态引入(可以理解为 异步的),但是下面这种写法在vue3中不行
// const AsyncComponent = () => import("./component/AsyncComponent.vue");
// 2、vue3中的同台引入
const AsyncComponent = defineAsyncComponent(
    () => import("./component/AsyncComponent.vue")
);
export default defineComponent({
    components: {
        AsyncComponent,
    },
    setup() {},
});
</script>

2)子组件

<template>
    <h2>异步组件</h2>
    {{msg}}
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
    setup() {
        // 在这模拟一个异步的操作(这里一般是数据请求),这块也可以使用async和await
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve({
                    msg: "你好!!!",
                });
            }, 2000);
        });
    },
    /**
    //async和await的数据请求如下
    async setup() {
        const request = await axios.get("路径");
        return {
            data:request
        }
    },
     */
});
</script>

3j3be-ykbn0.gif

七、结合路由

1、params路由传参

1)在路由文件里面定义规则(也就是确定传递的键名)

//下面这个是不传参数的,这样可以解决错误提示(必须写在参数的前面)
{ name: "details", path: "details", component: () => import("路径") },
//下面这个 id就是键名
{ name: "details", path: "details/:id", component: () => import("路径") }

image-20210216023154722

2)在跳转页面

<template>
    路由api
    <div>
    	<!-- 下面使用了二级路由 -->
        <div id="menu">
            <router-link v-for="item in list" :key="item.id" :to="'/router-api/details/' + item.id">{{item.title}}</router-link>
        </div>
        <div id="content">
            <router-view></router-view>
        </div>
    </div>
</template>
<script>
import { reactive } from 'vue'
export default {
    setup() {
        let list = reactive([
            { title: "列表一", id: 1 },
            { title: "列表二", id: 2 },
            { title: "列表三", id: 3 },
            { title: "列表四", id: 4 }
        ])
        return { list }
    }
}
</script>
<style scoped>
a{
    display: block;
    margin-top: 20px;
}
#menu {
    width: 30%;
    background: orchid;
    height: 300px;
    float: left;
}
#content {
    width: 70%;
    background: palegreen;
    height: 300px;
    float: right;
}
</style>

3)在接收页面

<template>
    详情
    <p>{{$route.params.id}}</p>
    {{id}}
    <p>{{detailsId}}</p>
</template>
<script>
import { ref, watch } from 'vue';
import { useRoute, useRouter } from "vue-router";
export default {
    setup() {
        const route = useRoute();
        const router = useRouter();
        let detailsId = ref(route.params.id);
        watch(()=>route.params,(news)=>{
            detailsId.value = news.id;
        })

        return { detailsId }
    },
    computed: {
        // 原来的方法
        id() {
            return this.$route.params.id
        }
    }
}
</script>

image-20210216023010910

2、query传参

1)跳转页面

<router-link :to="{path:'/router-api/article',query:{name:'啊啊啊',age:22}}">跳转</router-link>
<button @click="$router.push({path:'/router-api/article',query:{name:'哈哈哈',age:222}})">跳转</button>

2)接收页面

<template>
    文章
    <p>{{$route.query.name}}</p>

</template>
<script>
import { ref } from 'vue';
import { useRoute, useRouter } from "vue-router";
export default {
     setup() {
        const route = useRoute();
        const router = useRouter();
        //这里只执行一次,多个参数的时候,用监听
        console.log(route.query.name);
    },
}
</script>

注:导航守卫

onBeforeRouteLeave((to, from) => {})

onBeforeRouteUpdate(async (to, from) => {})

3、监听路由的变化

<template>
    
</template>
<script>
import { useRouter } from "vue-router";
export default {
    name: 'App',
    setup() {
        // 不需要菜单的路径数组
        const noMenu = [ '/login' ];
        const router = useRouter();
        const state = reactive({
            showMenu: true,// 是否需要显示菜单
        })
        router.beforeEach(to => {
            console.log(to);
            // includes() 方法用来判断一个数组是否包含一个指定的值,如果是返回 true,否则false。
            state.showMenu = !noMenu.includes(to.path);
        })
        return { ...toRefs(state) }
    }
}
</script>
//全局路由前置守卫(to是跳转后的页面路由,from是跳转前的,next是必须要执行的,加上next路由才会跳转)
router.beforeEach(async (to, from, next) => {
    await 要执行的方法
})

4、路由模式的切换

hash模式url里面永远带着#号

history模式没有#号

在router——>index.js里面的使用

import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router';

const router = createRouter({
    // history: createWebHashHistory(), // hash 模式
    history: createWebHistory(), // history模式(不带#号)
    routes: [
        {
            path: '路径',
            name: "index",
            component: import("路径地址")
        }
    ]
})

export default router;

八、引入外部样式

1、src——> 创建style文件

reset.css

/* 
https://meyerweb.com/eric/tools/css/reset/
*/
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
    margin: 0;
    padding: 0;
    border: 0;
    font-size: 100%;
    font: inherit;
    vertical-align: baseline;
}
​
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
    display: block;
}
​
body {
    line-height: 1;
}
​
ol,
ul {
    list-style: none;
}

blockquote,
q {
    quotes: none;
}

blockquote:before,
blockquote:after,
q:before,
q:after {
    content: "";
    content: none;
}

table {
    border-collapse: collapse;
    border-spacing: 0;
}

html,
body,
#app {
    width: 100%;
    height: 100%;
}

2、在页面引入

<template>
    <router-view />
</template>

<style lang="scss">
// 方法1
@import url("./style/reset.css");

// 方法2
@import "./style/reset.css";
</style>

九、引入svg图

1、下载

npm i --save svg-sprite-loader

2、在src——> 创建vag文件夹

vag文件里面包括 resources文件夹SvgIcon.vue

1)resources文件夹里面放svg图

2)SvgIcon.vue

<template>
    <svg :class="svgClass" aria-hidden="true">
        <use :xlink:href="iconName" />
    </svg>
</template>

<script lang="ts">
import { defineComponent, computed } from "vue";
export default defineComponent({
    name: "svg-icon",
    props: {
        iconClass: {
            type: String,
            required: true,
        },
        className: {
            type: String,
            default: "",
        },
    },
    setup(props) {
        const iconName = computed(() => `#icon-${props.iconClass}`);
        const svgClass = computed(() => {
            if (props.className) {
                return "svg-icon " + props.className;
            } else {
                return "svg-icon";
            }
        });
        return { iconName, svgClass };
    },
});
</script>

<style scoped>
.svg-icon {
    width: 1em;
    height: 1em;
    vertical-align: -0.15em;
    fill: currentColor;
    overflow: hidden;
}
</style>

3、在vue.config.js里面进项配置

const path = require('path')
function resolve(dir) {
    return path.join(__dirname, dir)
}
module.exports = {
    lintOnSave: false,
    chainWebpack(config) {
        config.module
            .rule('svg')
            .exclude.add(resolve('src/assets/svg'))
            .end()
        config.module
            .rule('icons')
            .test(/.svg$/)
            .include.add(resolve('src/assets/svg'))
            .end()
            .use('svg-sprite-loader')
            .loader('svg-sprite-loader')
            .options({
                symbolId: 'icon-[name]'
            })
            .end()
    }
}

4、在min.js里面引入

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

const app = createApp(App);
app.use(store);
app.use(router);
app.mount('#app');

/**
 * 导入svg图
 */
import SvgIcon from '@/assets/svg/SvgIcon.vue';
app.component("svg-icon", SvgIcon);
const requireAll = (requireContext: __WebpackModuleApi.RequireContext) => requireContext.keys().map(requireContext);
const req = require.context("./assets/svg/resources", false, /.svg$/);
requireAll(req);

5、使用

<template>
    <div class="about">
        <svg-icon icon-class="user" className="box" />
    </div>
</template>
<style scoped>
.box{
    width: 60px;
    height: 20px;
    margin-top: 15px;
}
</style>

十、在vue3中使用mock数据

注:以下这个写法可以在notWork中显示。

1、下载

npm install chalk --dev
npm install mockjs --dev

image-20210722160245380

2、在根目录下新建mock文件夹

注:在里面创建文件。

├── mock                     # 模拟数据
│   │── index.js             # 
│   │── mock-server.js       # 
│   │── utils.js             # 
└── └── 模拟的数据.js          # 

1)index.js

const Mock = require('mockjs');
const { param2Obj } = require('./utils');

const user = require('./user');
const table = require('./table');
const test = require("./test");
const mocks = [
    ...user,
    ...table,
    ...test
]

//用于前端模拟
//请谨慎使用,它会重新定义 XMLHttpRequest,这将导致您的许多第三方库失效(如进度事件)。
function mockXHR() {
    // mock patch
    // https://github.com/nuysoft/Mock/issues/300
    Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
    Mock.XHR.prototype.send = function () {
        if (this.custom.xhr) {
            this.custom.xhr.withCredentials = this.withCredentials || false

            if (this.responseType) {
                this.custom.xhr.responseType = this.responseType
            }
        }
        this.proxy_send(...arguments)
    }

    function XHR2ExpressReqWrap(respond) {
        return function (options) {
            let result = null
            if (respond instanceof Function) {
                const { body, type, url } = options
                // https://expressjs.com/en/4x/api.html#req
                result = respond({
                    method: type,
                    body: JSON.parse(body),
                    query: param2Obj(url)
                })
            } else {
                result = respond
            }
            return Mock.mock(result)
        }
    }

    for (const i of mocks) {
        Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
    }
}

module.exports = {
    mocks,
    mockXHR
}

2)mock-server.js

const chokidar = require('chokidar')
const bodyParser = require('body-parser')
const chalk = require('chalk')
const path = require('path')
const Mock = require('mockjs')

const mockDir = path.join(process.cwd(), 'mock')

function registerRoutes(app) {
    let mockLastIndex
    const { mocks } = require('./index.js')
    const mocksForServer = mocks.map(route => {
        return responseFake(route.url, route.type, route.response)
    })
    for (const mock of mocksForServer) {
        app[mock.type](mock.url, mock.response)
        mockLastIndex = app._router.stack.length
    }
    const mockRoutesLength = Object.keys(mocksForServer).length
    return {
        mockRoutesLength: mockRoutesLength,
        mockStartIndex: mockLastIndex - mockRoutesLength
    }
}

function unregisterRoutes() {
    Object.keys(require.cache).forEach(i => {
        if (i.includes(mockDir)) {
            delete require.cache[require.resolve(i)]
        }
    })
}

// 对于模拟服务器
const responseFake = (url, type, respond) => {
    console.log(url, type, respond);
    return {
        url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`),
        type: type || 'get',
        response(req, res) {
            console.log('request invoke:' + req.path)
            res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
        }
    }
}

module.exports = app => {
    // 解析 app.body
    // https://expressjs.com/en/4x/api.html#req.body
    app.use(bodyParser.json())
    app.use(bodyParser.urlencoded({
        extended: true
    }))

    const mockRoutes = registerRoutes(app)
    var mockRoutesLength = mockRoutes.mockRoutesLength
    var mockStartIndex = mockRoutes.mockStartIndex

    // 监视文件,热重载模拟服务器 
    chokidar.watch(mockDir, {
        ignored: /mock-server/,
        ignoreInitial: true
    }).on('all', (event, path) => {
        if (event === 'change' || event === 'add') {
            try {
                // 删除模拟路由堆栈 
                app._router.stack.splice(mockStartIndex, mockRoutesLength)

                // 清除路由缓存
                unregisterRoutes()

                const mockRoutes = registerRoutes(app)
                mockRoutesLength = mockRoutes.mockRoutesLength
                mockStartIndex = mockRoutes.mockStartIndex

                console.log(chalk.magentaBright(`\n > Mock Server 热加载成功!改变了  ${path}`))
            } catch (error) {
                console.log(chalk.redBright(error))
            }
        }
    })
}

3)utils.js

/**
 * @param {string} url
 * @returns {Object}
 */
function param2Obj(url) {
    const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
    if (!search) {
        return {}
    }
    const obj = {}
    const searchArr = search.split('&')
    searchArr.forEach(v => {
        const index = v.indexOf('=')
        if (index !== -1) {
            const name = v.substring(0, index)
            const val = v.substring(index + 1, v.length)
            obj[name] = val
        }
    })
    return obj
}

module.exports = {
    param2Obj
}

4)list.js(模拟数据)

const Mock = require('mockjs');

const data = Mock.mock({
    'items|30': [{
        id: '@id',
        title: '@sentence(10, 20)',
        'status|1': ['published', 'draft', 'deleted'],
        author: 'name',
        display_time: '@datetime',
        pageviews: '@integer(300, 5000)'
    }]
})

module.exports = [
    {
        url: '/list',
        type: 'get',
        response: config => {
            const items = data.items
            return {
                code: 20000,
                data: {
                    total: items.length,
                    items: items
                }
            }
        }
    }
]

3、配置环境

├── .env.development         # 开发
├── .env.production          # 生产
└── .env.staging             # 测试
开发生产测试
# 开发# 生产# 测试
ENV = 'development'ENV = 'production'ENV = 'staging'

base api |# base api|# base api

VUE_APP_BASE_API = '/dev-api' | VUE_APP_BASE_API = '/prod-api' | VUE_APP_BASE_API = '/stage-api'

4、使用

<template>
    <div class="about">
        
    </div>
</template>
<script>
import { onMounted } from '@vue/runtime-core';
import axios from "axios";
export default {
    setup() {
        onMounted(() => {
            axios({
                url: "/dev-api/list",
                method: "get"
            }).then(res => {
                console.log(res);
            })
        })
    }
}
</script>

注:如果要让其不在notWork显示的时候,在main.js写入以下代码

// 当为测试环境的时候,不在notWork中显示
if (process.env.NODE_ENV === 'production') {
    console.log("走这里");
    const { mockXHR } = require('../mock')
    mockXHR()
}