0
点赞
收藏
分享

微信扫一扫

前端笔记 - 从入门到深入了解 Vue2.0 及 Vue-router ---本地图片全没了,持续更新中

程序员伟杰 2022-04-13 阅读 8
vue.js

(一)Vue 模板语法

一、指令

1.1 什么是指令?

指令的本质就是自定义属性;指令的格式:以v-开始(比如: v-cloak)

1.2 v-cloak指令用法

  • 插值表达式存在的问题:“闪动”
  • 如何解决:使用v-cloak指令
  • 解决的原理:先隐藏,替换好值之后再显示最终的值

1.3 数据绑定指令

  • v-text:填充纯文本,相比插值表达式更加简洁
  • v-html:填充HTML片段,但是会存在安全问题,本网站内部数据可以使用该指令,来自第三方的数据不可以用
  • v-pre:填充原始信息,显示原始信息,跳过编译过程(用于分析编译过程)

1.4 数据响应式

  • 如何理解响应式:1. html5中的响应式(屏幕尺寸的变化导致样式的变化);2. 数据的响应式(数据的变化导致页面内容的变化)
  • 什么是数据绑定?数据绑定是将数据填充到标签中
  • v-once指令:只编译一次,显示内容之后不再具有响应式功能,其应用场景为当数据上传之后不需要再修改时可以提高编译效率

1.5 双向数据绑定(应用场景:表单输入)

请添加图片描述

二、 事件绑定

2.1 绑定事件

请添加图片描述
注意:
1)如果事件直接绑定函数名称,那么默认会传递事件对象作为事件函数的第一个参数;

2)如果事件绑定函数调用,那么事件对象必须作为最后一个参数显示传递,并且事件对象的名称必须是$event。

2.2 事件修饰符

<!-- .stop 阻止冒泡,相当于event.stopPropagation() -->
<a v-on:click.stop="handle">跳转</a>
<!-- .prevent 阻止默认行为,相当于event.preventDefault() -->
<a v-on:click.prevent="handle">跳转</a>

2.3 键盘事件

<!-- .enter 回车键 -->
<input v-on:keyup.enter="submit">
<!-- .delete 删除键 -->
<input v-on:keyup.delete="submit">

2.4 自定义按键修饰符

// 全局config.keyCodes对象
Vue.config.keyCodes.f1 = 112
Vue.config.keyCodes.名称 = event.keyCode

三、属性绑定

3.1 动态处理属性

  • v-bind 指令用法
<a v-bind:href="url">跳转</a>
<!-- 缩写形式 -->
<a :href="url">跳转</a>
<body>
  <div id="app">
    <a v-bind:href="url">百度</a>
    <a :href="url">百度1</a>
    <button v-on:click='handle'>切换</button>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    /*
      属性绑定
    */
    var vm = new Vue({
      el: '#app',
      data: {
        url: 'http://www.baidu.com'
      },
      methods: {
        handle: function(){
          // 修改URL地址
          this.url = 'http://itcast.cn';
        }
      }
    });
  </script>
</body>

3.2 v-model的底层实现原理

v-model的底层相当于使用了v-bind和v-on
<input v-bind: value="msg" v-on:input="msg=$event.target.value">
// 相当于在Vue对象中使用
this.msg = event.target.value;

四、样式绑定

4.1 class样式处理

  • 对象语法
<!-- isActive是控制active类名是否有效的变量,布尔类型 -->
<div v-bind:class="{ active: isActive }"></div>
  • 数组语法
<div v-bind:class="[ activeClass, errorClass ]"></div>
<div id="app">
        <div v-bind:class="{active: isActive}">测试</div>
        <button v-on:click="handle">切换</button>
</div>
    <script src="js/vue.js"></script>
    <script>
        // 样式绑定
        var vm = new Vue({
            el: '#app',
            data: {
                isActive: true
            },
            methods: {
                handle: function () {
                    this.isActive = !this.isActive;
                }
            }
        });
	 </script>
<div id="app">
     <div v-bind:class="[activeClass, errorClass]">测试</div>
     <button v-on:click="handle">切换</button>
</div>
<script src="js/vue.js"></script>
<script>
        // 第二种方法
        var vm = new Vue({
            el: '#app',
            data: {
                activeClass: 'active',
                errorClass: 'error'
            },
            methods: {
                handle: function () {
                    this.activeClass = '';
                    this.errorClass = '';
                }
            }
        });
</script>

对比: 数组绑定无法实现切换,用于添加多个类名的场景。

语法细节:

1、对象绑定和数组绑定可以结合使用

<div v-bind:class="[activeClass, errorClass,{test: isTest}]">测试1</div>
<div v-bind:class="arrClasses">测试2</div>
<div v-bind:class="objClasses">测试3</div>
data: {
        activeClass: 'active',
        errorClass: 'error',
		isTest: true,
		arrClasses: ['active','error'],
		objClasses: {
			active: true,
			error: true
		}
		methods: {
                handle: function () {
                    // this.isTest = false;
					this.objClasses.error = false;
                }
}

2、class绑定的值可以简化操作

3、默认的class如何处理? ---- 默认的class样式会被保留

4.2 style样式处理

  • 对象语法
<!-- "-"要用驼峰写法代替 -->
<div v-bind:style="{ color:activeColor, fontSize: fontSize}"></div>
  • 数组语法
<!-- 一般数组里面放置样式的对象 -->
<div v-bind:style="[ baseStyles, overridingStyles]"></div>
<div id="app">
        <div v-bind:style="{border: borderStyle, width: widthStyle, height: heightStyle}"></div>
        <div v-bind:style="[objStyle,overrideStyles]"></div>
        <div v-bind:style="objStyle"></div>
        <button v-on:click="handle">切换</button>
    </div>
    <script src="js/vue.js"></script>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                borderStyle: '1px solid blue',
                widthStyle: '100px',
                heightStyle: '200px',
                objStyle: {
                    border: '1px solid blue',
                    width: '200px',
                    height: '100px'
                },
                overrideStyles: {
                    border: '5px solid orange',
                    backgroundColor: 'blue'
                }
            },
            methods: {
                handle: function () {
                    this.heightStyle = '100px';
                    this.objStyle.width = '100px';
                }
            }
        });
    </script>

五、分支循环结构

5.1 v-show

原理:控制元素样式是否显示,操控display属性。

v-show与v-if的区别:

v-show会将结构渲染到页面,若不显示则display:none,而v-if的原理是是否渲染元素到页面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dOBGZP8X-1649484576768)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618197098743.png)]

<div v-if="score>=90">优秀</div>
<div v-else-if="score<90&&score>=80">良好</div>
<div v-else-if="score<80&&score>=60">一般</div>
<div v-else>比较差</div>
<div v-show="flag">测试</div>

5.2 v-for

在这里插入图片描述

// 使用原生js遍历对象
    var obj = {
      uname: 'lisi',
      age: 12,
      gender: 'male'
    }
    for(var key in obj) {
      console.log(key, obj[key])
    }
<div id="app">
    <div v-if='v==13' v-for='(v,k,i) in obj'>{{v + '---' + k + '---' + i}}</div>
  </div>
var vm = new Vue({
      el: '#app',
      data: {
        obj: {
          uname: 'zhangsan',
          age: 13,
          gender: 'female'
        }
      }
    });

5.3 声明式编程

  • 模板的结构和最终显示的效果基本一致

(二)Vue常用特性

六、表单操作

6.1 基于Vue的表单操作

<div id="app">
        <form action="http://itcast.cn">
            <div>
                <span>姓名:</span>
                <span>
                    <input type="text" v-model="uname">
                </span>
            </div>
            <div>
                // 设置value值区分不同选项,通过传值选中该选项框
                <span>性别:</span>
                <span>
                    <input type="radio" id="male" value="1" v-model="gender">
                    <label for="male"></label>
                    <input type="radio" id="female" value="2" v-model="gender">
                    <label for="female"></label>
                </span>
            </div>
            <div>
                <span>爱好:</span>
                <input type="checkbox" id="ball" value="1" v-model="hobby">
                <label for="ball">篮球</label>
                <input type="checkbox" id="sing" value="2" v-model="hobby">
                <label for="sing">唱歌</label>
                <input type="checkbox" id="code" value="3" v-model="hobby">
                <label for="code">写代码</label>
            </div>
            <div>
                <span>职业:</span>
                <select v-model="occupation" multiple>
                    <option value="0">请选择职业...</option>
                    <option value="1">教师</option>
                    <option value="2">软件工程师</option>
                    <option value="3">律师</option>
                </select>
            </div>
            <div>
                <span>个人简介:</span>
                // Vue不允许在标签中间写文本内容
                <textarea v-model="desc"></textarea>
            </div>
            <div>
                <!-- 禁止表单默认提交行为 -->
                <input type="submit" value="提交" @click.prevent="handle">
            </div>
        </form>
    </div>
    <script type="text/javascript" src="js/vue.js"></script>
    <script type="text/javascript">
        /*
        表单基本操作
        */
        var vm = new Vue({
            el: '#app',
            data: {
                uname: 'lisi',
                gender: 2,
                hobby: ['2', '3'],
                // occupation: 3
                occupation: ['2', '3'],
                desc: 'nihao'
            },
            methods: {
                handle: function () {
                    // console.log(this.uname);
                    // 输出数据
                    console.log(this.hobby.toString());
                }
            }
        });
    </script>

6.2 表单域修饰符

在这里插入图片描述
input事件和change事件的区别: change事件在失去焦点时触发,input事件在文本发生改变时触发。

七、自定义指令

在这里插入图片描述
带参数的自定义指令
在这里插入图片描述
在这里插入图片描述
与全局指令的区别: 局部指令只能在本组件(关于组件的概念在后面章节)中使用,全局指令则没有限制。

八、计算属性(基于data中的数据,数据改变,计算属性对应发生变化)

<div id="app">
        <div>{{msg}}</div>
        <div>{{reverseString}}</div>
    </div>
    <script src="js/vue.js"></script>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                msg: 'Hello'
            },
            computed: {
                reverseString: function () {
                    return this.msg.split('').reverse().join('');
                }
            }
        })
    </script>

在这里插入图片描述

九、侦听器

9.1 应用场景

数据变化时执行异步开销较大的操作。

在其他场景(开销小)下,用计算属性也能实现相同的功能,且更方便。
在这里插入图片描述

9.2 用法

在这里插入图片描述

十、过滤器

10.1 过滤器作用

在这里插入图片描述

10.2 自定义过滤器

过滤器也支持级联效果(链式编程)

<div id="app">
    <input type="text" v-model='msg'>
    <div>
        {{msg | upper}}
    </div>
</div>
<script>
	Vue.filter('upper',function(val){
        // 记得返回值,否则没有数据
        return val.charAt(0).toUpperCase() + val.slice(1);
    });
    var vm = new Vue({
        el: '#app',
        data: {
            msg: ''
        }
    });
</script>

10.3 过滤器的使用

在这里插入图片描述

10.4 局部过滤器

在这里插入图片描述

10.5 带参数的过滤器

在这里插入图片描述

<div id="app">
        <div>{{date | format('yyyy-MM-dd hh:mm:ss')}}</div>
    </div>
    <script src="js/vue.js"></script>
    <script>
        Vue.filter('format', function (value, arg) {
            function dateFormat(date, format) {
                if (typeof date === "string") {
                    var mts = date.match(/(\/Date\((\d+)\)\/)/);
                    if (mts && mts.length >= 3) {
                        date = parseInt(mts[2]);
                    }
                }
                date = new Date(date);
                if (!date || date.toUTCString() == "Invalid Date") {
                    return "";
                }
                var map = {
                    "M": date.getMonth() + 1, //月份 
                    "d": date.getDate(), //日 
                    "h": date.getHours(), //小时 
                    "m": date.getMinutes(), //分 
                    "s": date.getSeconds(), //秒 
                    "q": Math.floor((date.getMonth() + 3) / 3), //季度 
                    "S": date.getMilliseconds() //毫秒 
                };

                format = format.replace(/([yMdhmsqS])+/g, function (all, t) {
                    var v = map[t];
                    if (v !== undefined) {
                        if (all.length > 1) {
                            v = '0' + v;
                            v = v.substr(v.length - 2);
                        }
                        return v;
                    } else if (t === 'y') {
                        return (date.getFullYear() + '').substr(4 - all.length);
                    }
                    return all;
                });
                return format;
            }
            return dateFormat(value, arg);
        });
        var vm = new Vue({
            el: '#app',
            data: {
                date: new Date()
            },
            methods: {

            }
        });
    </script>

十一、生命周期

11.1 主要阶段

在这里插入图片描述

Vue 实例生命周期

11.2 Vue实例过程

在这里插入图片描述

11.3 Vue的数组方法(数组的响应式变化)

在这里插入图片描述

// 通过 list[1] = 'lemon' 和 obj.gender = 'male' 添加的值不具有响应式,即修改数组或对象不发生变化
要用 vm.$set(vm.items,1,'lemon') 和 vm.$set(vm.items,'gender','male') 进行添加响应式数据

(三)组件

一、组件化开发思想

在这里插入图片描述

二、组件注册

2.1 全局组件注册语法

在这里插入图片描述

2.2 用法

在这里插入图片描述

2.3 注意事项

1、data必须是一个函数:为了形成一个闭包,否则会报错

2、组件模板内容必须是单个根元素

3、组件模板内容可以是模板字符串

  • 模板字符串需要浏览器提供支持(ES6语法)
template: `
                <div>
                    <button @click="count++">点击了{{count}}次</button>
                    <button>测试</button>
                </div>
            `

2.4 组件命名方式

在这里插入图片描述

2.5 局部组件

!局部组件只能在注册它的父组件中使用
在这里插入图片描述

三、组件间数据交互

3.1 父组件向子组件传值

在这里插入图片描述
在这里插入图片描述
1、数值和布尔值不做属性绑定时,数据类型会变成string。

2、props传递数据原则:单向数据流

3.2 子组件向父组件传值

在这里插入图片描述

<div id="app">
        <div :style="{fontSize: fontSize + 'px'}">{{pmsg}}</div>
        <menu-item :parr="parr" @enlarge-text="handle($event)"></menu-item>
    </div>
    <script src="js/vue.js"></script>
    <script>
        Vue.component('menu-item', {
            props: ['parr'],
            template: `
            <div>
                <ul>
                    <li :key='index' v-for='(item,index) in parr'>{{item}}</li>
                </ul>
				// 虽然没有禁止子组件修改父组件的值,但是建议不要这样做
                <button @click='parr.push("lemon")'>点击</button>
                <button @click='$emit("enlarge-text",5)'>扩大父组件中的字体大小</button>
            </div>
        `
        });
        var vm = new Vue({
            el: '#app',
            data: {
                pmsg: '父组件中内容',
                parr: ['apple', 'orange', 'banana'],
                fontSize: 10
            },
            methods: {
                handle: function (val) {
                    // 扩大字体大小
                    this.fontSize += val;
                }
            }
        });
    </script>

3.3 非父子组件间传值(兄弟组件传值)

在这里插入图片描述

<div id="app">
        <div>父组件</div>
        <div>
            <button @click="handle">销毁事件</button>
        </div>
        <test-tom></test-tom>
        <test-jerry></test-jerry>
    </div>
    <script src="js/vue.js"></script>
    <script>
        // 提供事件中心
        var hub = new Vue();
        Vue.component('test-tom', {
            data: function () {
                return {
                    num: 0
                }
            },
            template: `
                <div>
                    <div>TOM:{{num}}</div>
                    <div>
                        <button @click='handle'>点击</button>
                    </div>
                </div>
            `,
            methods: {
                handle: function () {
                    // 触发兄弟组件的事件
                    hub.$emit('jerry-event', 2)
                }
            },
            mounted: function () {
                // 监听事件
                hub.$on('tom-event', (val) => {
                    this.num += val;
                })
            }
        });
        Vue.component('test-jerry', {
            data: function () {
                return {
                    num: 0
                }
            },
            template: `
                <div>
                    <div>JERRY:{{num}}</div>
                    <div>
                        <button @click='handle'>点击</button>
                    </div>
                </div>
            `,
            methods: {
                handle: function () {
                    hub.$emit('tom-event', 1)
                }
            },
            mounted: function () {
                // 监听事件
                hub.$on('jerry-event', (val) => {
                    this.num += val;
                });
            }
        });
        var vm = new Vue({
            el: '#app',
            data: {

            },
            methods: {
                handle: function () {
                    hub.$off('tom-event');
                    hub.$off('jerry-event');
                }
            }
        });
    </script>

3.4 组件插槽

作用:
在这里插入图片描述
在这里插入图片描述

<slot>默认内容</slot>

3.5 具名插槽

在这里插入图片描述
在这里插入图片描述

3.6 作用域插槽

在这里插入图片描述

<div id="app">
        <fruit-list :list="list">
            <template slot-scope="slotProps">
                <strong v-if="slotProps.info.id == 2" class="current">{{slotProps.info.name}}</strong>
                <span v-else>{{slotProps.info.name}}</span>
            </template>
        </fruit-list>
    </div>
    <script src="js/vue.js"></script>
    <script>
        Vue.component('fruit-list', {
            props: ['list'],
            template: `
                <div>
                    <li :key='item.id' v-for='item in list'>
                        <slot :info="item">{{item.name}}</slot>
                    </li>
                </div>
            `
        });
        var vm = new Vue({
            el: '#app',
            data: {
                list: [{
                    id: 1,
                    name: 'apple'
                }, {
                    id: 2,
                    name: 'banana'
                }, {
                    id: 3,
                    name: 'orange'
                }]
            }
        });
    </script>

(四)前后端交互

一、前后端交互模式

1.1 URL地址格式

在这里插入图片描述

二、Promise 用法

2.1 异步调用

在这里插入图片描述
在这里插入图片描述
注:会出现回调地狱现象。

2.2 Promise概述(Promise本身是函数,函数也是对象)

在这里插入图片描述

var p = new Promise(function (resolve, reject) {
            // 这里用于实现异步任务
            setTimeout(function () {
                var flag = true;
                if (flag) {
                    // 正常情况
                    resolve('hello');
                } else {
                    // 异常情况
                    reject('error');
                }
            }, 100);
        });
        p.then(function (data) {
            console.log(data);
        }, function (info) {
            console.log(info);
        });

2.3 基于Promise处理Ajax请求

function queryData(url) {
            var p = new Promise(function (resolve, reject) {
                var xhr = new XMLHttpRequest();
                xhr.onreadystatechange = function () {
                    if (xhr.readyState != 4) return;
                    if (xhr.readyState == 4 && xhr.status == 200) {
                        // 处理正常情况
                        resolve(xhr.responseText);
                    } else {
                        // 处理异常情况
                        reject('服务器错误');
                    }
                };
                xhr.open('get', url);
                xhr.send(null);
            });
            return p;
        }
        queryData('http://localhost:3000/data')
            .then(function (data) {
                console.log(data);
            }, function (info) {
                console.log(info);
            });

发送多次Ajax请求并且保证顺序:

queryData('http://localhost:3000/data')
            .then(function (data) {
                console.log(data);
                return queryData('http://localhost:3000/data1');
            })
            .then(function (data) {
                console.log(data);
                return queryData('http://localhost:3000/data2');
            })
            .then(function (data) {
                console.log(data);
            });

2.4 then参数中的函数返回值

在这里插入图片描述

2.5 Promise常用的API

在这里插入图片描述

三、接口调用-fetch用法

3.1 fetch 概述

在这里插入图片描述

3.2 fetch 基本用法

fetch('http://localhost:3000/fdata').then(function (data) {
    	// text(方法属于fetch API的一部分,它返回一个Promise实例对象,用于获取后台返回的数据
            return data.text();
        }).then(function (data) {
            console.log(data);
        });

3.3 fetch 请求参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kEmODRmh-1649487890482)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618626921540.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NGjbEmnm-1649487890483)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618626969205.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pxbvGkvC-1649487890483)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618627098248.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vs9FhTwA-1649487890484)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618627161070.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d6OypgHh-1649487890485)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618627212143.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G4Y1bLgv-1649487890485)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618627366962.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NnofX2JN-1649487890486)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618627598592.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wq44k8Nr-1649487890486)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618627649256.png)]

! 允许跨域访问设置

// 设置允许跨域访问该服务
app.all('*', function (req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS');
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
  res.header('Access-Control-Allow-Headers', 'Content-Type');
  // 开启认证请求
  res.header('Access-Control-Allow-Credentials',true);
  // 内容类型:如果是post请求必须指定这个属性
  res.header('Content-Type','application/json;charset=utf-8');
  next();
});

3.4 fetch 响应结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dk3kR8J6-1649484576796)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618628920123.png)]

res.json()

https://www.expressjs.com.cn/4x/api.html#res.json

res.json({
    uname: 'lisi',
    pwd: '123'
});

四、接口调用-axios用法

4.1 axios 的基本特性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yo6sWDrZ-1649484576796)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618629778322.png)]

4.2 axios 的基本用法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9HviIVx6-1649484576797)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618629934924.png)]

4.3 axios 参数传递

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jtjenFEU-1649484576797)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618630565043.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K7sfTndm-1649484576798)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618630585083.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ph16O1b7-1649484576798)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618630659929.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-clFVGf06-1649484576799)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618630849934.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1f5R1LgJ-1649484576799)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618631025745.png)]

4.4 axios的响应结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vVm7kpsv-1649484576799)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618631119083.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LJJesPHK-1649484576800)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618631135460.png)]

4.5 axios的全局配置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jji52oJx-1649484576800)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618631256346.png)]

res.header('Access-Control-Allow-Headers', 'mytoken');
/* 全局配置 */
        // 配置请求的基准URL地址,会自动拼接上baseURL
        axios.defaults.baseURL = 'http://localhost:3000/';
        // 配置请求头信息
        axios.defaults.headers['mytoken'] = 'hello';
        axios.get('axios-json').then(function (ret) {
            console.log(ret.data.uname);
        });

4.6 axios拦截器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jI2QSmST-1649484576801)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618631825359.png)]

axios.interceptors.request.use(function (config) {
            // 可用于让某些路由设置请求头
            console.log(config.url);
            config.headers.mytoken = 'nihao';
            return config;
        }, function (err) {
            console.log(err);
        });
        axios.get('http://localhost:3000/adata').then(function (data) {
            console.log(data.data);
        })

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ow6EzhPX-1649484576801)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1619357634795.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IhBGXybn-1649484576801)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618632212976.png)]

/* 响应拦截器 */
        axios.interceptors.response.use(function (res) {
            // res 和下面的 data是完全一样的对象
            var data = res.data;
            return res;
        }, function (err) {
            console.log(err);
        });
        axios.get('http://localhost:3000/adata').then(function (data) {
            // 这里就可以直接打印data了
            console.log(data);
        })

4.7 axios带token下载文件

exportFile(event) {
      event.preventDefault()
      //使a自带的方法失效,即无法调整到href中的URL
      this.$http({
        method: 'get',
        url: 'http://anjude.cn.utools.club/cashout/export',
        responseType: 'blob',
      })
        .then((res) => {
          const link = document.createElement('a')
          let blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
          console.log(res)
          //获取heads中的filename文件名
          var fileName = '订单列表'
          link.style.display = 'none'
          link.href = URL.createObjectURL(blob)
          link.setAttribute('download', fileName)
          document.body.appendChild(link)
          link.click()
          document.body.removeChild(link)
        })
        .catch((error) => {
          console.log(error)
        })
    },

五、接口调用-async/await用法

5.1 async/await 的基本用法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2phlqZIk-1649484576802)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618632544516.png)]

async function queryData() {
            var ret = await axios.get('http://localhost:3000/adata');
            // console.log(ret.data);
            return ret.data;
        }
        queryData().then(function (data) {
            console.log(data);
        });

// 后面跟Promise对象用法
async function queryData() {
            var ret = await new Promise(function(resolve,reject){
                setTimeout(function(){
                    resolve('nihao');
                },1000);
            });
            return ret;
        }
        queryData().then(function (data) {
            console.log(data);
        });

5.2 async/await 处理多个异步请求

多个异步请求场景

/* 处理多个异步请求 */
        axios.defaults.baseURL = 'http://localhost:3000/';

        async function queryData() {
            var info = await axios.get('async1');
            var ret = await axios.get('async2?info=' + info.data);
            return ret.data;
        }

        queryData().then(function (data) {
            console.log(data);
        });

(四)Vue路由

一、路由的基本概念

1.1 路由

分为前端路由和后端路由。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i6W4BubP-1649484576802)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618650271050.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m5Yugl3M-1649484576803)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618650312273.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S4KGgUPz-1649484576803)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618650345982.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4WJNLo1u-1649484576804)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618650433704.png)]

哈希值:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SKcqMYs1-1649484576804)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618650828716.png)]

!component占位符:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uCbjzXwe-1649484576805)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618650971320.png)]

1.2 Vue Router

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-355D6kve-1649484576805)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618651092964.png)]

二、Vue-Router基本使用

2.1 步骤

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tDLBPTTb-1649484576806)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618651178627.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y1OU2mA5-1649484576806)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618651812492.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9d4Zjayr-1649484576807)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618651824881.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cTQMvnUX-1649484576807)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618651844666.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ckAUrw1u-1649484576807)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618651855902.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pbn2tJa1-1649484576808)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618651892739.png)]

2.2 路由重定向

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sRD8Gbur-1649484576808)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618652096405.png)]

三、嵌套路由

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xGKcLGvX-1649484576809)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618658428745.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8bU8AlWC-1649484576809)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618658443648.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V43kHNet-1649484576809)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618658457022.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eo3fXfdH-1649484576810)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618658467142.png)]

四、动态路由匹配

4.1 动态匹配路由的基本用法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O0HMCbh5-1649484576810)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618659053019.png)]

4.2 路由组件传递参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ePlwx0Es-1649484576810)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618659196060.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ka7jzfXb-1649484576811)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618661147571.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zXPCwJxk-1649484576811)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618661177961.png)]

五、命名路由

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RY2njlB5-1649484576811)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618661495136.png)]

<router-link :to="{name: 'user',params: {id: 3}}">User3</router-link>

六、编程式导航

6.1 页面导航的两种方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yRZbBR8Q-1649484576812)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618661716064.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VjLxHe4i-1649484576812)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618661757078.png)]

6.2 router.push()参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g5HzSrm1-1649484576812)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618662701277.png)]

6.3 路由导航守卫控制访问权限

6.3.1 全局前置守卫

1. 旧版导航守卫(使用next),这里的next用法和router.push()用法一样。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AG6xdKa4-1649484576813)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1619353195850.png)]

2. 新版导航守卫根据return值判断导航是否有效:(当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于等待中)

a. false: 取消当前的导航。如果浏览器的URL改变了(可能是用户手动或者浏览器后退按钮),那么URL地址会重置到from路由对应的地址。

b. 一个路由地址: 通过一个路由地址跳转到一个不同的地址,就像调用router.push()方法一样,可以设置诸如 replace: true或name: 'home’之类的配置。当前导航被中断,然后进行一个新的导航,就和from一样

c. undefined 或 true: 导航是有效的,并调用下一个导航守卫。

d. 意料之外的情况,可能会抛出一个error。这会取消导航并且调用 router.onError()注册过的回调

router.beforeEach(async(to,from) => {
    // canUserAccess()返回 true 或 false
    return await canUserAccess(to)
})
6.3.2 全局解析守卫

你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,因为它在 每次导航时都会触发,但是确保在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被正确调用。这里有一个例子,确保用户可以访问自定义 meta 属性 requiresCamera 的路由:

router.beforeResolve(async to => {
  if (to.meta.requiresCamera) {
    try {
      await askForCameraPermission()
    } catch (error) {
      if (error instanceof NotAllowedError) {
        // ... 处理错误,然后取消导航
        return false
      } else {
        // 意料之外的错误,取消导航并把错误传给全局处理器
        throw error
      }
    }
  }
})
6.3.3 全局后置钩子

全局后置钩子和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身,它们对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。

router.afterEach((to, from) => {
  sendToAnalytics(to.fullPath)
})

它们也反映了 navigation failures 作为第三个参数:

router.afterEach((to, from, failure) => {
  if (!failure) sendToAnalytics(to.fullPath)
})

6.3.4 路由独享守卫

a. 可以直接在路由配置上定义 beforeEnter 守卫:(用法类似beforeEach)

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from) => {
      // reject the navigation
      return false
    },
  },
]

beforeEnter 守卫 只在进入路由时触发,不会在 paramsqueryhash 改变时触发。例如,从 /users/2 进入到 /users/3 或者从 /users/2#info 进入到 /users/2#projects。它们只有在 从一个不同的 路由导航时,才会被触发。

b. 可以将一个函数数组传递给 beforeEnter,这在为不同的路由重用守卫时很有用:

function removeQueryParams(to) {
  if (Object.keys(to.query).length)
    return { path: to.path, query: {}, hash: to.hash }
}

function removeHash(to) {
  if (to.hash) return { path: to.path, query: to.query, hash: '' }
}

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: [removeQueryParams, removeHash],
  },
  {
    path: '/about',
    component: UserDetails,
    beforeEnter: [removeQueryParams],
  },
]

6.3.5 路由组件钩子
  • beforeRouteEnter
  • beforeRouteUpdate
  • beforeRouteLeave
const UserDetails = {
  template: `...`,
  beforeRouteEnter(to, from) {
    // 在渲染该组件的对应路由被验证前调用
    // 不能获取组件实例 `this` !
    // 因为当守卫执行时,组件实例还没被创建!
  },
  beforeRouteUpdate(to, from) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
    // 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
  },
  beforeRouteLeave(to, from) {
    // 在导航离开渲染该组件的对应路由时调用
    // 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
  },
}

beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。

不过,你可以通过传一个回调给 next 来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数:

beforeRouteEnter (to, from, next) {
  next(vm => {
    // 通过 `vm` 访问组件实例
  })
}

注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdatebeforeRouteLeave 来说,this 已经可用了,所以** 不支持 传递回调,因为没有必要了:

beforeRouteUpdate (to, from) {
  // just use `this`
  this.name = to.params.name
}

这个 离开守卫 通常用来预防用户在还未保存修改前突然离开。该导航可以通过返回 false 来取消。

beforeRouteLeave (to, from) {
  const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
  if (!answer) return false
}

6.4 基于token的退出登录功能

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FJeqUTXI-1649484576813)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1619353614277.png)]

6.5 Vue-Router注意点

  1. 使用带有参数(/users/:username)的路由时需要注意的是,相同的组件实例被重复使用,比起销毁再创建,复用则显得更加高效。**不过,这也意味着组件的生命周期钩子不会被调用。**可用以下方法解决:
// 1. watch监听$route对象上的任意属性,比如监听$route.params
const User = {
    template: '...',
    created(){
        this.$watch(
        	() => this.$route.params,
            (toParams,previousParams) => {
                // 对路由变化做出响应
            }
        )
        /*
            // watch 路由的参数,以便再次获取数据
            this.$watch(
              () => this.$route.params,
              () => {
                this.fetchData()
              },
              // 组件创建完后获取数据,
              // 此时 data 已经被 observed 了
              { immediate: true }
            )
        */
    }
}
// 2. 利用导航守卫
const User = {
    template: '...',
    async beforeRouteUpdate(to, from){
        // 对路由变化做出响应
        this.userData = await fetchUser(to.params.id)
    }
}
  1. 嵌套路由时,当想在子路由未匹配时也渲染一些东西呈现时,可以提供一个空的嵌套路径:
const routes = [
    {
        path: '/user/:id',
        component: User,
        children: [
            // 当 /user/:id 匹配成功
            // UserHome 将被渲染到User的<router-view>内部
            {
                path: '', component: UserHOme
            },
            // ...其他子路由
        ]
    }
]

  1. 想要导航到不同的URL,可以使用router.push(this.$router.push)方法,这个方法会向history栈添加一个新的记录,所以当用户点击浏览器后退按钮时,会回到之前的URL。
  2. router.replace()的作用与router.push()类似,唯一的不同是,它在导航时不会向history添加新记录。
// 直接在传递给router.push的routeLocation中增加一个属性replace: true 相当于router.replace()
router.push({ path: '/home', replace: true })
// 相当于
router.replace({ path: 'home' })

  1. 横跨历史
window.history.go(n) // 该方法采用一个整数作为参数,表示在历史堆栈中前进或后退多少步

// 向前移动一条记录,与 router.forward() 相同
router.go(1)

// 返回一条记录,与router.back() 相同
router.go(-1)

// 前进 3 条记录
router.go(3)

// 如果没有那么多记录,静默失败
router.go(-100)
router.go(100)

  1. 路由别名
  1. 路由的props

a. 将props传递给路由组件

const User = {
  template: '<div>User {{ $route.params.id }}</div>'
}
const routes = [{ path: '/user/:id', component: User }]

// ========== 替换为 ===========

const User = {
    props: ['id'],
    template: '<div>User {{ id }}</div>'
}
const routes = [{ path: '/user/:id', component: User, props: true }] // 当 props 设置为 true 时,route.params 将被设置为组件的 props。

b. 对于有命名视图的路由,必须为每个命名视图定义props配置:

const routes = [
  {
    path: '/user/:id',
    components: { default: User, sidebar: Sidebar },
    props: { default: true, sidebar: false }
  }
]

c. 当props是一个对象时,它将原样设置为组件props。当props是静态的时候很有用。

const routes = [
  {
    path: '/promotion/from-newsletter',
    component: Promotion,
    props: { newsletterPopup: false }
  }
]

d. 你可以创建一个返回 props 的函数。这允许你将参数转换为其他类型,将静态值与基于路由的值相结合等等。

const routes = [
    {
        path: '/search',
        component: SearchUser,
        props: route => ({ query: route.query.q })
    }
]

**注意:**请尽可能保持 props 函数为无状态的,因为它只会在路由发生变化时起作用。如果你需要状态来定义 props,请使用包装组件,这样 vue 才可以对状态变化做出反应。

  1. 推荐使用这个模式:用 createWebHistory() 创建 HTML5 模式的历史模式。

6.6 完整导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  5. 在路由配置里调用 beforeEnter。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter。
  8. 调用全局的 beforeResolve 守卫(2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

6.7 路由元信息

const routes = [
  {
    path: '/posts',
    component: PostsLayout,
    children: [
      {
        path: 'new',
        component: PostsNew,
        // 只有经过身份验证的用户才能创建帖子
        meta: { requiresAuth: true }
      },
      {
        path: ':id',
        component: PostsDetail
        // 任何人都可以阅读文章
        meta: { requiresAuth: false }
      }
    ]
  }
]

如何访问meta属性?

router.beforeEach((to, from) => {
  // 而不是去检查每条路由记录
  // to.matched.some(record => record.meta.requiresAuth)
  if (to.meta.requiresAuth && !auth.isLoggedIn()) {
    // 此路由需要授权,请检查是否已登录
    // 如果没有,则重定向到登录页面
    return {
      path: '/login',
      // 保存我们所在的位置,以便以后再来
      query: { redirect: to.fullPath },
    }
  }
})

TypeScript

// typings.d.ts or router.ts
import 'vue-router'

declare module 'vue-router' {
  interface RouteMeta {
    // 是可选的
    isAdmin?: boolean
    // 每个路由都必须声明
    requiresAuth: boolean
  }
}

6.8 滚动行为

使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 vue-router 能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。

注意: 这个功能只在支持 history.pushState 的浏览器中可用。

当创建一个 Router 实例,你可以提供一个 scrollBehavior 方法:

const router = createRouter({
    history: createWebHashHistory(),  // 推荐使用这个模式:用createWebHistory()创建 HTML5 模式的历史模式
    routes: [...],
    scrollBehavior(to,from,savedPosition){
        	 // return 期望滚动到哪个位置
             }
})

scrollBehavior 函数接收 to from 路由对象,如 Navigation Guards。第三个参数 savedPosition,只有当这是一个 popstate 导航时才可用(由浏览器的后退/前进按钮触发)。

该函数可以返回一个 ScrollToOptions 位置对象:

const router = createRouter({
  scrollBehavior(to, from, savedPosition) {
    // 始终滚动到顶部
    return { top: 0 }
  },
})

也可以通过 el 传递一个 CSS 选择器或一个 DOM 元素。在这种情况下,topleft 将被视为该元素的相对偏移量。

const router = createRouter({
  scrollBehavior(to, from, savedPosition) {
    // 始终在元素 #main 上方滚动 10px
    return {
      // 也可以这么写
      // el: document.getElementById('main'),
      el: '#main',
      top: -10,
    }
  },
})

如果返回一个 falsy 的值,或者是一个空对象,那么不会发生滚动。

返回 savedPosition,在按下 后退/前进 按钮时,就会像浏览器的原生表现那样:

const router = createRouter({
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { top: 0 }
    }
  },
})

如果你要模拟 “滚动到锚点” 的行为:

const router = createRouter({
  scrollBehavior(to, from, savedPosition) {
    if (to.hash) {
      return {
        el: to.hash,
      }
    }
  },
})

如果你的浏览器支持滚动行为,你可以让它变得更流畅:

const router = createRouter({
  scrollBehavior(to, from, savedPosition) {
    if (to.hash) {
      return {
        el: to.hash,
        behavior: 'smooth',
      }
    }
  }
})

延迟滚动

有时候,需要在页面中滚动之前稍作等待。例如,当处理过渡时,我们希望等待过渡结束后再滚动。要做到这一点,你可以返回一个 Promise,它可以返回所需的位置描述符。下面是一个例子,我们在滚动前等待 500ms:

const router = createRouter({
    scrollBehavior(to,from,savedPosition){
        return new Promise((resolve,reject)=>{
            setTimeout(()=>{
                resolve({left: 0, top: 0})
            },500)
        })
    },
})

可以将其与页面级过渡组件的事件挂钩,以使滚动行为与你的页面过渡很好地结合起来,但由于使用场景可能存在的差异和复杂性,这里只是提供了这个基础来实现特定的用户场景。

6.9 路由懒加载

// 将 import UseDetails = () => import('./views/UserDetails') 替换成
const UserDetails = () => import('./views/UserDetails')

const router = createRouter({
    // ...
    routes: [{ path: '/users/:id', component: UserDetails }],
})

component (和 components) 配置接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。这意味着你也可以使用更复杂的函数,只要它们返回一个 Promise :

const UserDetails = () =>
  Promise.resolve({
    /* 组件定义 */
  })

一般来说,对所有的路由都使用动态导入是个好主意。

注意: 不要在路由中使用异步组件。异步组件仍然可以在路由组件中使用,但路由组件本身就是动态导入的。

如果你使用的是 webpack 之类的打包器,它将自动从代码分割中受益。

如果你使用的是 Babel,你将需要添加 syntax-dynamic-import 插件,才能使 Babel 正确地解析语法。

6.10 把组件按组分块

有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4):

const UserDetails = () => import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
const UserDashboard = () => import(/* webpackChunkName: "group-user" */ './UserDashboard.vue')
const UserProfileEdit = () => import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue')

webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。

6.11 导航故障

当使用 router-link 组件时,Vue Router 会自动调用 router.push 来触发一次导航。虽然大多数链接的预期行为是将用户导航到一个新页面,但也有少数情况下用户将留在同一页面上:

  • 用户已经位于他们正在尝试导航到的页面
  • 一个导航守卫通过调用 return false 中断了这次导航
  • 当前的导航守卫还没有完成时,一个新的导航守卫会出现了
  • 一个导航守卫通过返回一个新的位置,重定向到其他地方 (例如,return '/login')
  • 一个导航守卫抛出了一个 Error
6.11.1 检测导航故障

如果导航被阻止,导致用户停留在同一个页面上,由 router.push 返回的 Promise 的解析值将是 Navigation Failure。否则,它将是一个 falsy 值(通常是 undefined)。这样我们就可以区分我们导航是否离开了当前位置:

const navigationResult = await router.push('/my-profile')
if(navigationResult){
    // 导航被阻止
} else {
    // 导航成功(包括重新导航的情况)
    this.isMenuOpen = false // 示例,关闭菜单栏
}

6.11.2 鉴别导航故障
import { NavigationFailureType, isNavigationFailure } from 'vue-router'

// 试图离开未保存的编辑文本界面
const failure = await router.push('/articles/2')

if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
  // 给用户显示一个小通知
  showToast('You have unsaved changes, discard and leave anyway?')
}

6.11.3 导航故障的属性
// 正在尝试访问 admin 页面
router.push('/admin').then(failure => {
    if(isNavigationFailure(failure, NavigationFailureType.redirected)){
        failure.to.path // '/admin'
        failure.from.path // '/'
    }
})

6.11.4 检测重定向

当在导航守卫中返回一个新的位置时,我们会触发一个新的导航,覆盖正在进行的导航。与其他返回值不同的是,重定向不会阻止导航,而是创建一个新的导航。因此,通过读取路由地址中的 redirectedFrom 属性,对其进行不同的检查:

await router.push('/my-profile')
if (router.currentRoute.value.redirectedFrom) {
  // redirectedFrom 是解析出的路由地址,就像导航守卫中的 to和 from
}

6.12 动态路由

对路由的添加通常是通过 routes 选项来完成的,但是在某些情况下,你可能想在应用程序已经运行的时候添加或删除路由。具有可扩展接口(如 Vue CLI UI )这样的应用程序可以使用它来扩展应用程序。

6.12.1 添加路由

动态路由主要通过两个函数实现。router.addRoute()router.removeRoute()。它们注册一个新的路由,也就是说,如果新增加的路由与当前位置相匹配,就需要你用 router.push()router.replace()手动导航,才能显示该新路由。我们来看一个例子:

想象一下,只有一个路由的以下路由:

const router = createRouter({
  history: createWebHistory(),
  routes: [{ path: '/:articleName', component: Article }],
})

进入任何页面,/about/store,或者 /3-tricks-to-improve-your-routing-code 最终都会呈现 Article 组件。如果我们在 /about 上添加一个新的路由:

router.addRoute({ path: '/about', component: About })

页面仍然会显示 Article 组件,我们需要手动调用 router.replace() 来改变当前的位置,并覆盖我们原来的位置(而不是添加一个新的路由,最后在我们的历史中两次出现在同一个位置):

router.addRoute({ path: '/about', component: About })
// 我们也可以使用 this.$route 或 route = useRoute() (在 setup 中)
router.replace(router.currentRoute.value.fullPath)

记住,如果你需要等待新的路由显示,可以使用 await router.replace()

6.12.2 在导航守卫中添加路由

如果你决定在导航守卫内部添加或删除路由,你不应该调用 router.replace(),而是通过返回新的位置来触发重定向:

router.beforeEach(to => {
  if (!hasNecessaryRoute(to)) {
    router.addRoute(generateRoute(to))
    // 触发重定向
    return to.fullPath
  }
})

上面的例子有两个假设:第一,新添加的路由记录将与 to 位置相匹配,实际上导致与我们试图访问的位置不同。第二,hasNecessaryRoute() 在添加新的路由后返回 false,以避免无限重定向。

因为是在重定向中,所以我们是在替换将要跳转的导航,实际上行为就像之前的例子一样。而在实际场景中,添加路由的行为更有可能发生在导航守卫之外,例如,当一个视图组件挂载时,它会注册新的路由。

6.12.3 删除路由

有几个不同的方法来删除现有的路由:

  • 通过添加一个名称冲突的路由。如果添加与现有途径名称相同的途径,会先删除路由,再添加路由:

    router.addRoute({ path: '/about', name: 'about', component: About })
    // 这将会删除之前已经添加的路由,因为他们具有相同的名字且名字必须是唯一的
    router.addRoute({ path: '/other', name: 'about', component: Other })
    
    
  • 通过调用router.addRoute()返回的回调:

    const removeRoute = router.addRoute(routeRecord)
    removeRoute() // 删除路由如果存在的话
    
    

    当路由没有名称时,这很有用。

  • 通过使用router.removeRoute()按名称删除路由:

    router.addRoute({ path: '/about', name: 'about', component: About })
    // 删除路由
    router.removeRoute('about')
    
    

    需要注意的是,如果你想使用这个功能,但又想避免名字的冲突,可以在路由中使用Symbol作为名字。

    TIP: 当路由被删除时,所有的别名和子路由也会被同时删除

6.12.4 添加嵌套路由

要将嵌套路由添加到现有的路由中,可以将路由的 name 作为第一个参数传递给 router.addRoute(),这将有效地添加路由,就像通过 children 添加的一样:

router.addRoute({ name: 'admin', path: '/admin', component: Admin })
router.addRoute('admin', { path: 'settings', component: AdminSettings })

这等效于:

router.addRoute({
  name: 'admin',
  path: '/admin',
  component: Admin,
  children: [{ path: 'settings', component: AdminSettings }],
})
6.12.5 查看现有路由

Vue Router 提供了两个功能来查看现有的路由:

  • router.hasRoute():检查路由是否存在。
  • router.getRoutes():获取一个包含所有路由记录的数组。

七、token原理分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HCs13OKM-1649484576814)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1619336782848.png)]

八、.prettierrc.json

{
    // 解决与ESlint的冲突
    "semi":false,
    "singleQuote": true,
        // 换行设置
    "printWidth": 200
}

九、Element-UI

1、el-row

一行有24格,通过 :span=“6” 添加每一列所占格数,使所有列格数相加等于24。

2、阻止启动生产消息

Vue.config.productionTip = false

3、改变el-input的宽高

/deep/ .el-input__inner {

​ width: 400px;

​ height: 60px;

​ }

改变prefix-icon大小:

/deep/.el-icon-user:before {

​ font-size: 18px;

​ }

4、$nextTick

当页面上元素被重新渲染之后,才会执行回调函数中的代码。

this.$nextTick((_) => {
        this.$refs.saveTagInput.$refs.input.focus()
      })

5、上传图片时携带token

// axios
// 确认上传功能
    submitUpload() {
      this.$refs.videoFormRef.validate(async (valid) => {
        if (!valid || !this.imgInfo.name || !this.videoInfo.name) {
          this.$message.error('请填写完整的表单数据')
        } else {
          let params = 'source=' + this.videoForm.uploadPathValue + '&videoName=' + this.videoForm.fileName
          // 创建空的formData表单对象
          var formData = new FormData()
          // 将用户选择的文件追加到formData表单对象中
          formData.append('file', this.imgInfo)
          formData.append('video', this.videoInfo)
          const { data: res } = await this.$http.put('file/video?' + params, formData)
          if (res.status !== 200) return this.$message.error('上传视频失败,请重新登录后尝试')
          this.$message.success('上传视频成功')
          this.$router.push('/video')
        }
      })
    },
        
// 自定义上传路径
        this.$http({
        method: 'put',
        url: 'https://acfly.cn/fibreboard/message/poster',
        data: formData,
      })
        .then((res) => {
          if (res.status !== 200) return this.$message.error('上传海报失败')
          this.$message.success('上传海报成功')
        })
        .catch((error) => {
          console.log(error)
        })

6、自行注册Element组件

// element.js
import Timeline from './timeline/index.js'
import TimelineItem from './timeline-item/index.js'
Vue.use(Timeline)
Vue.use(TimelineItem)

// 使用的.vue文件
<style lang="less" scoped>
@import '../../plugins/timeline/timeline.css';
@import '../../plugins/timeline-item/timeline-item.css';
</style>

7、echarts

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gVZbsa2a-1649484576814)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1623144196310.png)]

// 1.导入echarts
import * as echarts from 'echarts'
let echarts from 'echarts'
// 2.<!-- 为ECharts准备一个具备大小(宽高)的Dom -->
<div id="main" style="width: 600px; height: 400px"></div>
// 3.基于准备好的dom,初始化echarts实例
mounted() {
    // 基于准备好的dom,初始化echarts实例
    var myChart = echarts.init(document.getElementById('main'))
  },
 const { data: res } = await this.$http.get(`reports/type/1`)
// 4.准备数据和配置项
    const result = _.merge(res.data, this.options)
// 5.展示数据
myChart.setOption(result)


// 配置项
options: {
        title: {
          text: '用户来源'
        },
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'cross',
            label: {
              backgroundColor: '#E9EEF3'
            }
          }
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: [
          {
            boundaryGap: false
          }
        ],
        yAxis: [
          {
            type: 'value'
          }
        ]
      }

十、vue项目初始化

1、vue ui

2、创建新项目 --> 选择文件夹 --> 填写项目名、init project

3、安装插件:vue-cli-plugin-element --> 配置插件: import on demand

4、依赖:axios --> less@4.1.1 --> less-loader@5.0.0 --> .prettierrc.json

5、添加公钥:设置–> SSH公钥 ssh-keygen -t rsa -C “xxxxx@xxxxx.com” --> 黏贴生成SSH --> ssh -T git@gitee.com

6、新建仓库:去掉使用Readme文件对勾 -->

git config --global user.name "AhhC"
git config --global user.email "2388639967@qq.com"

–> 根据仓库指令绑定仓库

git多公钥ssh连接多仓库

1、本地新建创建多个ssh公钥
通过git bash打开命令行

进入的ssh公钥配置目录
cd ~/.ssh

2、新建新的ssh公钥
//新建demo@gmail.com的demo公钥
ssh-keygen -t rsa -C “demo@gmail.com” -f demo

注意:实际的邮件地址和-f后面的公钥名称demo根据自己的情况命名,邮件地址跟ssh私钥的能对应就OK了

3、新建config

//新建config
touch config

4、编辑config
config配置信息如下:

默认的git配置,如gitee.com,Host是主机可自行定义名称

Host gitee.com
HostName gitee.com
User deme1@gmail.com
IdentityFile ~/.ssh/id_rsa

Host gitee.demo.com
HostName gitee.com
User demo2@gmail.com
IdentityFile ~/.ssh/demo

5、把配置的ssh公钥复制到git对应后台生成私钥
git地址的变化
git地址,如:git@gitee.com:demo/test.git
按照config的配置:Host:gitee.com,git clone命令如下:

git clone git@gitee.com:demo/test.git

其中Host:gitee.demo.com的git地址如下:

git clone git@gitee.demo.com:demo/test.git

注:第二个git的地址,跟你实际获得的git地址有所不同,主要根据config配置文件的Host来遍历具体的git地址及用户和公钥等信息
注意:vue-cli的项目名称不能包含大写

十二、项目优化

12.1 项目优化策略

  1. 生成打包报告
  2. 第三方库启用CDN
  3. ElementUI组件按需加载
  4. 路由懒加载
  5. 首页内容定制

12.2 nprogress

1、vue-cli下载运行依赖
2、main.js导入文件
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
3// 在request拦截器中,展示进度条
axios.interceptors.request.use(config=>{
  NProgress.start()
  config.headers.Authorization = window.sessionStorage.getItem('token');
  return config;
})
// 在response拦截器中,隐藏进度条
axios.interceptors.response.use(config=>{
  NProgress.done()
  return config
})

12.3 去除console语句

安装开发依赖:babel-plugin-transform-remove-console

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uaGUDPSa-1649484576815)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1620719853313.png)]

// 由于babel.config.js是全局配置,所以不论开发环境还是生产环境都会启用依赖,所以进行优化:
// 这是项目发布阶段需要用到的babel插件
const prodPlugins = []
if(process.env.NODE_ENV === 'production'){
  prodPlugins.push('transform-remove-console')
}

module.exports = {
  "presets": [
    "@vue/app"
  ],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ],
    // 发布产品时候的插件数组
    ...prodPlugins
  ]
}

12.4 生产打包报告

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p9oPepVJ-1649484576815)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1620720317690.png)]

12.5 通过 vue.config.js 修改Webpack的默认配置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lWQSZYaF-1649484576815)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1620720536402.png)]

12.6 为开发模式与发布模式指定不同的打包入口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l1VStHcP-1649484576816)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1620720628257.png)]

configureWebpack和chainWebpack:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3kUfKr7A-1649484576816)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1620720807316.png)]

12.7 通过chainWebpack自定义打包入口

module.exports = {
  lintOnSave: false,
  chainWebpack: config => {
    config.when(process.env.NODE_ENV === 'production', config => {
      config.entry('app').clear().add('./src/main-prod.js')
    })

    config.when(process.env.NODE_ENV === 'development', config => {
      config.entry('app').clear().add('./src/main-dev.js')
    })
  }
}

12.8 通过externals加载外部CDN资源

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BKJqihzt-1649484576816)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1620999997546.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mbdjKQGV-1649484576817)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1621000014104.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gd4HELO3-1649484576817)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1621000026720.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RronYpLq-1649484576817)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1621000034961.png)]

12.9 通过CDN优化ElementUI的打包

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FoVIzUAe-1649484576818)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624158584055.png)]

<!-- element-ui 的样式表文件 -->
<link rel="stylesheet" href="https://cdn.staticfile.org/element-ui/2.8.2/theme-chalk/index.css" />
<!-- element-ui 的js文件 -->
<script src="https://cdn.staticfile.org/element-ui/2.8.2/index.js"></script>

12.10 自定义首页内容

chainWebpack: config => {
    // 发布模式
    config.when(process.env.NODE_ENV === 'production', config => {
      config.entry('app').clear().add('./src/main-prod.js')

      config.plugin('html').tap(args => {
        args[0].isProd = true
        return args
      })
    })

    // 开发模式
    config.when(process.env.NODE_ENV === 'development', config => {
      config.entry('app').clear().add('./src/main-dev.js')

      config.plugin('html').tap(args => {
        args[0].isProd = false
        return args
      })
    })
  }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D8LY44Bi-1649484576818)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624159357320.png)]

12.11 实现路由懒加载

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PsjLuG9G-1649484576818)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624159760135.png)]

关于路由懒加载的详细文档,可参考如下链接:
https://router.vuejs.org/zh/guide/advanced/lazy-loading.html

// babel.config.js
"plugins": ['@babel/plugin-syntax-dynamic-import']
// router.js
// 同一个chunkName表示会打包到同一个JS文件中
const Login = () => import(/* webpackChunkName: "login_home_welcome" */ './components/Login.vue')

十三、常用方法

13.1 自定义时间格式处理过滤器

Vue.filter('dataFormat',function(originVal){
  const dt = new Date(originVal)

  const y = dt.getFullYear()
  const m = (dt.getMonth() + 1 + '').padStart(2,'0')
  const d = (dt.getDate() + '').padStart(2,'0')

  const hh = (dt.getHours() + '').padStart(2,'0')
  const mm = (dt.getMinutes() + '').padStart(2,'0')
  const ss = (dt.getSeconds() + '').padStart(2,'0')

  return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
})

注:或者用moment插件

13.2 下载Excel文件

exportFile(event) {
      event.preventDefault() //使a自带的方法失效,即无法调整到href中的URL
      this.$http({
        method: 'get',
        url: 'https://www.jingyun.club/background/message/export',
        responseType: 'blob',
      })
        .then((res) => {
          const link = document.createElement('a')
          let blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
          console.log(res)
          //获取heads中的filename文件名
          var fileName = '信息收集列表'
          link.style.display = 'none'
          link.href = URL.createObjectURL(blob)
          link.setAttribute('download', fileName)
          document.body.appendChild(link)
          link.click()
          document.body.removeChild(link)
        })
        .catch((error) => {
          console.log(error)
        })
    },

13.3 a标签携带token

 //方式1
		$(".a_post").on("click",function(event){
		event.preventDefault();//使a自带的方法失效,即无法调整到href中的URL
		var url='http://localhost:8050/file/export/snapEventVO';   //请求的URl
			var xhr = new XMLHttpRequest();		//定义http请求对象
			xhr.open("POST", url, true);		
			xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
			xhr.send();
			xhr.responseType = "blob";  // 返回类型blob
			xhr.onload = function() {   // 定义请求完成的处理函数,请求前也可以增加加载框/禁用下载按钮逻辑
				if (this.status===200) {
					var blob = this.response;
					//alert(this.readyState);
					//alert(xhr.getAllResponseHeaders());
					console.log(xhr.getResponseHeader("content-disposition"))
					let temp = xhr.getResponseHeader("content-disposition").split(";")[1].split("filename=")[1];
					var fileName = decodeURIComponent(temp);
					//var hh = xhh.getResponseHeader("fileName");
				
					//var fileName = this.response.headers["content-disposition"].split(";")[1].split("filename=")[1];
					//console.log("fileName="+fileName)
					//console.log(xhr.getResponseHeader("content-disposition"))
					var reader = new FileReader();
					reader.readAsDataURL(blob);  // 转换为base64,可以直接放入a标签href
					reader.οnlοad=function (e) {
						console.log(e);			//查看有没有接收到数据流
						// 转换完成,创建一个a标签用于下载
						var a = document.createElement('a');
						a.download=fileName+".xlsx";			//自定义下载文件名称
						a.href = e.target.result;
						$("body").append(a);    // 修复firefox中无法触发click
						a.click();
						//$(a).remove();
					}
				}
				else{
					alert("出现了未知的错误!");
				}
			}
		});
 
        //方式2
		$(".a_post").on("click",function(event){
			event.preventDefault();//使a自带的方法失效,即无法调整到href中的URL
			axios({
				method: 'post',
				url: "http://localhost:8050/file/export/snapEventVO",
				responseType: 'blob'
			}).then((res) => {
				const link = document.createElement('a')
				let blob = new Blob([res.data],{type: 'application/vnd.ms-excel'});
			//获取heads中的filename文件名
			var aa = res.headers["content-disposition"]
			let temp = res.headers["content-disposition"].split(";")[1].split("filename=")[1];
			var fileName = decodeURIComponent(temp);
			console.log(fileName)
			link.style.display = 'none'
			link.href = URL.createObjectURL(blob);
			link.setAttribute('download', fileName)
			document.body.appendChild(link)
			link.click()
			document.body.removeChild(link)
		}).catch(error => {
				console.log(error)
			})
		});

十四、vue-quill-editor

安装:npm install vue-quill-editor --save

main.js文件中
// 导入富文本编辑器
import VueQuillEditor from 'vue-quill-editor'
// require styles
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'

// 将富文本编辑器注册为全局可用的组件
Vue.use(VueQuillEditor)

// ---------------使用: 客户端SPA--------------
<!-- 富文本编辑器组件 -->
<quill-editor v-model="addForm.goods_introduce"></quill-editor>

// 在全局样式表中更改样式,局部样式更改无效
.ql-editor {
    min-height: 300px;
}

十五、lodash

// 在所需组件中引用
<script>
    import _ from 'lodash'
    // 1.深拷贝
    const form = _.cloneDeep(this.addForm)
    // 2.对象合并
    const result = _.merge(res.data, this.options)
</script>

// JSON方法,只要不是null和undefined就不会报错
    JSON.parse(JSON.stringify(this.addForm))

十六、项目上线

16.1 通过node创建web服务器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dC9AVbir-1649484576819)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624160978907.png)]

16.2 开启 gzip 配置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l9aCOCOI-1649484576819)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624161155639.png)]

16.3 配置HTTPS服务(默认端口 443)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RtvZUkgi-1649484576819)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624161273229.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2KmPNDsT-1649484576820)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624161310207.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-manv9AHQ-1649484576821)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624161425842.png)]

16.4 使用pm2管理应用(关闭服务器终端后仍可以访问)

在服务器中写

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7V0SLPG2-1649484576821)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624161748906.png)]

十七、Vue-Router:vue2迁移到vue3(即vue-router3到vue-router4)

1、new Router 变成 createRouter

// 以前是
// import Router from 'vue-router'
import { createRouter } from 'vue-router'

// const router = new Router({})
const router = createRouter({
  // ...
})

2、新的history配置代替mode

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

createRouter({
    history: createWebHistory(),
    routes: []
})

在 SSR 上使用时,你需要手动传递相应的 history:

// router.js
let history = isServer ? createMemoryHistory() : createWebHistory()
let router = createRouter({ routes, history })
// 在你的server-entry.js 中的某个地方
router.push(req.url)  // 请求url
router.isReady().then(()=>{
    // 处理请求
})

原因:为未使用的 history 启用摇树,以及为高级用例(如原生解决方案)实现自定义 history。

3、移动了base配置

// 原来
new Router({
    mode: 'history',  // 访问路径不带#号
    base: '/page/aa', // 配置单页应用的基路劲
})

// 这时,页面访问 http://localhost:8080/page/aa 和 http://localhost:8080/的效果是一样的。
import { createRouter, createWebHistory } from 'vue-router'
createRouter({
  history: createWebHistory('/base-directory/'),
  routes: [],
})

4、删除了 RouterOptions 中的 fallback属性

创建路由时不再支持fallback属性:

-new VueRouter({
+createRouter({
-  fallback: false,
// other options...
})

原因: Vue支持的所有浏览器都支持 HTML5 History API,因此我们不再需要使用 location.hash,而可以直接使用 history.pushState()

5、删除了*通配符路由

现在必须使用自定义的 regex 参数来定义所有路由(*/*):在参数中自定义正则

onst routes = [
  // pathMatch 是参数的名称,例如,跳转到 /not/found 会得到
  // { params: { pathMatch: ['not', 'found'] } }
  // 这要归功于最后一个 *,意思是重复的参数,如果你
  // 打算直接使用未匹配的路径名称导航到该路径,这是必要的
  { path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound },
  // 如果你省略了最后的 `*`,在解析或跳转时,参数中的 `/` 字符将被编码
  { path: '/:pathMatch(.*)', name: 'bad-not-found', component: NotFound },
]
// 如果使用命名路由,不好的例子:
router.resolve({
  name: 'bad-not-found',
  params: { pathMatch: 'not/found' },
}).href // '/not%2Ffound'
// 好的例子:
router.resolve({
  name: 'not-found',
  params: { pathMatch: ['not', 'found'] },
}).href // '/not/found'

原因:Vue Router 不再使用 path-to-regexp,而是实现了自己的解析系统,允许路由排序并实现动态路由。由于我们通常在每个项目中只添加一个通配符路由,所以支持 * 的特殊语法并没有太大的好处。参数的编码是跨路由的,无一例外,让事情更容易预测。

6、将 onReady 改为 isReady

现有的 router.onReady() 函数已被 router.isReady() 取代,该函数不接受任何参数并返回一个 Promise:

// 将
router.onReady(onSuccess, onError)
// 替换成
router.isReady().then(onSuccess).catch(onError)
// 或者使用 await:
try {
  await router.isReady()
  // 成功
} catch (err) {
  // 报错
}

7、<router-view><keep-alive><transition>

transitionkeep-alive 现在必须通过 v-slot API 在 RouterView 内部使用:

<router-view v-slot="{ Component }">
  <transition>
    <keep-alive>
      <component :is="Component" />
    </keep-alive>
  </transition>
</router-view>

原因: 这是一个必要的变化。

8、删除 <router-link>中的eventtag属性

<router-link> 中的 eventtag 属性都已被删除。你可以使用 v-slot API 来完全定制 <router-link>

<router-link to="/about" tag="span" event="dblclick">About Us</router-link>
替换成
<router-link to="/about" custom v-slot="{ navigate }">
  <span @click="navigate" @keypress.enter="navigate" role="link">About Us</span>
</router-link>

原因:这些属性经常一起使用,以使用与 <a> 标签不同的东西,但这些属性是在 v-slot API 之前引入的,并且没有足够的使用,因此没有足够的理由为每个人增加 bundle 包的大小。

9、忽略mixins中的导航守卫

目前不支持mixins中的导航守卫,可以在 vue-router#454 追踪它的支持情况。

10、所有的导航都是异步的

所有的导航,包括第一个导航,现在都是异步的,这意味着,如果你使用一个 transition,你可能需要等待路由 ready 好后再挂载程序:

app.use(router)
// 注意:在服务器端,你需要手动跳转到初始地址。
router.isReady().then(() => app.mount('#app'))

否则会有一个初始过渡,就像你提供了 appear 属性到 transition 一样,因为路由会显示它的初始地址(什么都没有),然后显示第一个地址。

请注意,如果在初始导航时有导航守卫,你可能不想阻止程序渲染,直到它们被解析,除非你正在进行服务器端渲染。否则,在这种情况下,不等待路由准备好挂载应用会产生与 Vue2 中相同的结果。

11、将内容传递给路由组件的 <slot>

之前可以直接传递一个模板,通过嵌套在 <router-view> 组件下,由路由组件的 <slot> 来渲染:

<router-view>
  <p>In Vue Router 3, I render inside the route component</p>
</router-view>

由于 <router-view> 引入了 v-slot API,你必须使用 v-slot API 将其传递给 <component>

<router-view v-slot="{ Component }">
  <component :is="Component">
    <p>In Vue Router 3, I render inside the route component</p>
  </component>
</router-view>

12、不存在的命名路由 || 命名路由缺少必要的params

  1. 跳转或解析不存在的命名路由会产生错误:
// 哎呀,我们的名字打错了
router.push({ name: 'homee' }) // 报错
router.resolve({ name: 'homee' }) // 报错

原因:以前,路由会导航到 /,但不显示任何内容(而不是主页)。抛出一个错误更有意义,因为我们不能生成一个有效的 URL 进行导航。

  1. 在没有传递所需参数的情况下跳转或解析命名路由,会产生错误:
// 给与以下路由:
const routes = [{ path: '/users/:id', name: 'user', component: UserDetails }]

// 缺少 `id` 参数会失败
router.push({ name: 'user' })
router.resolve({ name: 'user' })

十八、Provide / Inject

1. 概述

对于这种情况,我们可以使用一对 provideinject。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。

Provide/inject scheme

例如我们有这样的层次结构:

Root
└─ TodoList
   ├─ TodoItem
   └─ TodoListFooter
      ├─ ClearTodosButton
      └─ TodoListStatistics

如果要将 todo-items 的长度直接传递给 TodoListStatistics,我们可以用provide/inject的方式,直接执行以下操作:

const app = Vue.createApp({})

app.component('todo-list', {
    data(){
        return {
            todos: ['Feed a cat', 'Buy tickets']
        }
    },
    provide: {
        user: 'John Doe'
    },
    template: `
		<div>
			{{ todos.length }}
		</div>
	`
})

app.component('todo-list-statistics', {
  inject: ['user'],
  created() {
    console.log(`Injected property: ${this.user}`) // > 注入的 property: John Doe
  }
})

但是,如果尝试在此处 provide 一些组件的实例 property,这将是不起作用的:

app.component('todo-list', {
  data() {
    return {
      todos: ['Feed a cat', 'Buy tickets']
    }
  },
  provide: {
    todoLength: this.todos.length // 将会导致错误 `Cannot read property 'length' of undefined`
  },
  template: `
    ...
  `
})

访问组件实例property,我们需要将provide转换为返回对象的函数

app.component('todo-list', {
  data() {
    return {
      todos: ['Feed a cat', 'Buy tickets']
    }
  },
  provide() {
    return {
      todoLength: this.todos.length
    }
  },
  template: `
    ...
  `
})

这使我们能够更安全地继续开发该组件,而不必担心可能会更改/删除子组件所依赖的某些内容。这些组件之间的接口仍然是明确定义的,就像 prop 一样。

2. 处理响应性

在上面的例子中,如果我们更改了 todos 的列表,这个变化并不会反映在 inject 的 todoLength property 中。这是因为默认情况下,provide/inject 绑定并不是响应式的。我们可以通过传递一个 ref property 或 reactive 对象给 provide 来改变这种行为。在我们的例子中,如果我们想对祖先组件中的更改做出响应,我们需要为 provide 的 todoLength 分配一个组合式 API computed property:

app.component('todo-list', {
  // ...
  provide() {
    return {
      todoLength: Vue.computed(() => this.todos.length)
    }
  }
})

app.component('todo-list-statistics', {
  inject: ['todoLength'],
  created() {
    console.log(`Injected property: ${this.todoLength.value}`) // > 注入的 property: 5
  }
})

在这种情况下,任何对 todos.length 的改变都会被正确地反映在注入 todoLength 的组件中。

十九、渲染函数

1. 概述

假设我们要生成一些带锚点的标题:

<h1>
    <a name="hello-world" href="#hello-world">
        Hello world!
    </a>
</h1>

锚点标题的使用非常频繁,我们应该创建一个组件:

<anchored-heading :level="1">Hello world!</anchored-heading>

1

当开始写一个只能通过 level prop 动态生成标题 (heading) 的组件时,我们很快就可以得出这样的结论:

const { createApp } = Vue
const app = createApp({})

app.component('anchored-heading', {
  template: `
    <h1 v-if="level === 1">
      <slot></slot>
    </h1>
    <h2 v-else-if="level === 2">
      <slot></slot>
    </h2>
    <h3 v-else-if="level === 3">
      <slot></slot>
    </h3>
    <h4 v-else-if="level === 4">
      <slot></slot>
    </h4>
    <h5 v-else-if="level === 5">
      <slot></slot>
    </h5>
    <h6 v-else-if="level === 6">
      <slot></slot>
    </h6>
  `,
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

这个模板感觉不太好。它不仅冗长,而且我们为每个级别标题重复书写了 <slot></slot>。当我们添加锚元素时,我们必须在每个 v-if/v-else-if 分支中再次重复它。

虽然模板在大多数组件中都非常好用,但是显然在这里它就不合适了。那么,我们来尝试使用 render 函数重写上面的例子:

const { createApp, h } = Vue
const app = createApp({})

app.component('anchored-heading', {
    render() {
        return h(
        	'h' + this.level, // 标签名
            {}, // prop 或 attribute
            this.$slots.default() // 包含其子节点的数组
        )
    },
    props: {
        level: {
            type: Number,
            required: true
        }
    }
})

render() 函数的实现要精简得多,但是需要非常熟悉组件的实例 property。在这个例子中,你需要知道,向组件中传递不带 v-slot 指令的子节点时,比如 anchored-heading 中的 Hello world! ,这些子节点被存储在组件实例中的 $slots.default 中。

2. 虚拟 DOM 树

Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。请仔细看这行代码:

return h('h1', {}, this.blogTitle)

h() 到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为 VNode。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。

// @returns {VNode}
h(
  // {String | Object | Function} tag
  // 一个 HTML 标签名、一个组件、一个异步组件、或
  // 一个函数式组件。
  //
  // 必需的。
  'div',

  // {Object} props
  // 与 attribute、prop 和事件相对应的对象。
  // 这会在模板中用到。
  //
  // 可选的。
  {},

  // {String | Array | Object} children
  // 子 VNodes, 使用 `h()` 构建,
  // 或使用字符串获取 "文本 VNode" 或者
  // 有插槽的对象。
  //
  // 可选的。
  [
    'Some text comes first.',
    h('h1', 'A headline'),
    h(MyComponent, {
      someProp: 'foobar'
    })
  ]
)

如果没有 prop,那么通常可以将 children 作为第二个参数传入。如果会产生歧义,可以将 null 作为第二个参数传入,将 children 作为第三个参数传入。

约束

VNodes必须唯一,若要创建重复很多次的元素/组件,可以使用工厂函数来实现,例如:

render(){
    return h('div',
        Array.from({length: 20}).map(()=> {
        	return h('p', 'hi')
    	})    
    )
}

3. 创建组件VNode

要为某个组件创建一个 VNode,传递给 h 的第一个参数应该是组件本身。

render() {
  return h(ButtonCounter)
}

如果我们需要通过名称来解析一个组件,那么我们可以调用 resolveComponent

const { h, resolveComponent } = Vue

// ...

render() {
  const ButtonCounter = resolveComponent('ButtonCounter')
  return h(ButtonCounter)
}

resolveComponent 是模板内部用来解析组件名称的同一个函数。

4. 使用 JavaScript 代替模板功能

v-model

v-model 指令扩展为 modelValueonUpdate:modelValue 在模板编译过程中,我们必须自己提供这些 prop:

props: ['modelValue'],
emits: ['update:modelValue'],
render() {
  return h(SomeComponent, {
    modelValue: this.modelValue,
    'onUpdate:modelValue': value => this.$emit('update:modelValue', value)
  })
}
v-on

我们必须为事件处理程序提供一个正确的 prop 名称,例如,要处理 click 事件,prop 名称应该是 onClick

render(){
    return h('div', {
        onClick: $event => console.log('clicked', $event.target)
    })
}
事件修饰符

对于 .passive.capture.once 事件修饰符,可以使用驼峰写法将他们拼接在事件名后面:

render() {
  return h('input', {
    onClickCapture: this.doThisInCapturingMode,
    onKeyupOnce: this.doThisOnce,
    onMouseoverOnceCapture: this.doThisOnceInCapturingMode
  })
}

对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:

修饰符 处理函数中的等价操作
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
按键: .enter, .13 if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码
修饰键: .ctrl, .alt, .shift, .meta if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKey, shiftKey, 或 metaKey)

这里是一个使用所有修饰符的例子:

render() {
  return h('input', {
    onKeyUp: event => {
      // 如果触发事件的元素不是事件绑定的元素
      // 则返回
      if (event.target !== event.currentTarget) return
      // 如果向上键不是回车键,则终止
      // 没有同时按下按键 (13) 和 shift 键
      if (!event.shiftKey || event.keyCode !== 13) return
      // 停止事件传播
      event.stopPropagation()
      // 阻止该元素默认的 keyup 事件
      event.preventDefault()
      // ...
    }
  })
}

插槽

你可以通过 this.$slots 访问静态插槽的内容,每个插槽都是一个VNode数组:

render() {
  // `<div><slot></slot></div>`
  return h('div', {}, this.$slots.default())
}

props: ['message'],
render() {
  // `<div><slot :text="message"></slot></div>`
  return h('div', {}, this.$slots.default({
    text: this.message
  }))
}

要使用渲染函数将插槽传递给子组件,请执行以下操作:

const { h, resolveComponent } = Vue

render() {
  // `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
  return h('div', [
    h(
      resolveComponent('child'),
      {},
      // 将 `slots` 以 { name: props => VNode | Array<VNode> } 的形式传递给子对象。
      {
        default: (props) => h('span', props.text)
      }
    )
  ])
}

插槽以函数的形式传递,允许子组件控制每个插槽内容的创建。任何响应式数据都应该在插槽函数内访问,以确保它被注册为子组件的依赖关系,而不是父组件。相反,对 resolveComponent 的调用应该在插槽函数之外进行,否则它们会相对于错误的组件进行解析。

// `<MyButton><MyIcon :name="icon" />{{ text }}</MyButton>`
render() {
  // 应该是在插槽函数外面调用 resolveComponent。
  const Button = resolveComponent('MyButton')
  const Icon = resolveComponent('MyIcon')

  return h(
    Button,
    null,
    {
      // 使用箭头函数保存 `this` 的值
      default: (props) => {
        // 响应式 property 应该在插槽函数内部读取,
        // 这样它们就会成为 children 渲染的依赖。
        return [
          h(Icon, { name: this.icon }),
          this.text
        ]
      }
    }
  )
}

如果一个组件从它的父组件中接收到插槽,它们可以直接传递给子组件。

render() {
  return h(Panel, null, this.$slots)
}

也可以根据情况单独传递或包裹住。

render() {
  return h(
    Panel,
    null,
    {
      // 如果我们想传递一个槽函数,我们可以通过
      header: this.$slots.header,

      // 如果我们需要以某种方式对插槽进行操作,
      // 那么我们需要用一个新的函数来包裹它
      default: (props) => {
        const children = this.$slots.default ? this.$slots.default(props) : []

        return children.concat(h('div', 'Extra child'))
      }
    }
  )
}

<component><is>

在底层实现里,模板使用 resolveDynamicComponent 来实现 is attribute。如果我们在 render 函数中需要 is 提供的所有灵活性,我们可以使用同样的函数:

const { h, resolveDynamicComponent } = Vue

// ...

// `<component :is="name"></component>`
render() {
  const Component = resolveDynamicComponent(this.name)
  return h(Component)
}

就像 is, resolveDynamicComponent 支持传递一个组件名称、一个 HTML 元素名称或一个组件选项对象。

通常这种程度的灵活性是不需要的。通常 resolveDynamicComponent 可以被换做一个更直接的替代方案。

例如,如果我们只需要支持组件名称,那么可以使用 resolveComponent 来代替。

如果 VNode 始终是一个 HTML 元素,那么我们可以直接把它的名字传递给 h

// `<component :is="bold ? 'strong' : 'em'"></component>`
render() {
  return h(this.bold ? 'strong' : 'em')
}

同样,如果传递给 is 的值是一个组件选项对象,那么不需要解析什么,可以直接作为 h 的第一个参数传递。

<template> 标签一样,<component> 标签仅在模板中作为语法占位符需要,当迁移到 render 函数时,应被丢弃。

自定义指令

可以使用 withDirectives 将自定义指令应用于 VNode:

const { h, resolveDirective, withDirectives } = Vue

// ...

// <div v-pin:top.animate="200"></div>
render () {
  const pin = resolveDirective('pin')

  return withDirectives(h('div'), [
    [pin, 200, 'top', { animate: true }]
  ])
}

resolveDirective 是模板内部用来解析指令名称的同一个函数。只有当你还没有直接访问指令的定义对象时,才需要这样做。

内置组件

诸如 <keep-alive><transition><transition-group><teleport> 等内置组件默认并没有被全局注册。这使得打包工具可以 tree-shake,因此这些组件只会在被用到的时候被引入构建。不过这也意味着我们无法通过 resolveComponentresolveDynamicComponent 访问它们。

在模板中这些组件会被特殊处理,即在它们被用到的时候自动导入。当我们编写自己的 render 函数时,需要自行导入它们:

const { h, KeepAlive, Teleport, Transition, TransitionGroup } = Vue
// ...
render () {
  return h(Transition, { mode: 'out-in' }, /* ... */)
}

5. 渲染函数的返回值

render 函数返回的是单个根 VNode。但其实也有别的选项。

返回一个字符串时会创建一个文本 VNode,而不被包裹任何元素:

render() {
  return 'Hello world!'
}

我们也可以返回一个子元素数组,而不把它们包裹在一个根结点里。这会创建一个片段 (fragment):

// 相当于模板 `Hello<br>world!`
render() {
  return [
    'Hello',
    h('br'),
    'world!'
  ]
}

可能是因为数据依然在加载中的关系,组件不需要渲染,这时它可以返回 null。这样我们在 DOM 中会渲染一个注释节点。

6. JSX

如果你写了很多渲染函数,可能会觉得下面这样的代码写起来很痛苦:

h(
  'anchored-heading',
  {
    level: 1
  },
  {
    default: () => [h('span', 'Hello'), ' world!']
  }
)

特别是对应的模板如此简单的情况下:

<anchored-heading :level="1"> <span>Hello</span> world! </anchored-heading>

这就是为什么会有一个 Babel 插件,用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。

import AnchoredHeading from './AnchoredHeading.vue'

const app = createApp({
  render() {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})

app.mount('#demo')

7. 函数式组件

函数式组件是自身没有任何状态的组件的另一种形式。它们在渲染过程中不会创建组件实例,并跳过常规的组件生命周期。

我们使用的是一个简单函数,而不是一个选项对象,来创建函数式组件。该函数实际上就是该组件的 render 函数。而因为函数式组件里没有 this 引用,Vue 会把 props 当作第一个参数传入:

const FunctionalComponent = (props, context) => {
  // ...
}

第二个参数 context 包含三个 property:attrsemitslots。它们分别相当于实例的 $attrs$emit$slots 这几个 property。

大多数常规组件的配置选项在函数式组件中都不可用。然而我们还是可以把 propsemits 作为 property 加入,以达到定义它们的目的:

FunctionalComponent.props = ['value']
FunctionalComponent.emits = ['click']

如果这个 props 选项没有被定义,那么被传入函数的 props 对象就会像 attrs 一样会包含所有 attribute。除非指定了 props 选项,否则每个 prop 的名字将不会基于驼峰命名法被一般化处理。

函数式组件可以像普通组件一样被注册和消费。如果你将一个函数作为第一个参数传入 h,它将会被当作一个函数式组件来对待。

举报

相关推荐

0 条评论