Vue 笔记

一、Vue 概述

渐进式 JavaScript 框架

声明式渲染 -> 组件系统 -> 客户端路由 -> 集中式状态管理 -> 项目构建。

官网:https://cn.vuejs.org/

  • 易用:熟悉 HTML、CSS、JavaScript 知识后,可快速上手 Vue
  • 灵活:在一个库和一套完整框架之间自如伸缩
  • 高效:20kb运行大小,超快虚拟 DOM

二、Vue 基本使用

<body>
    <div id="app">
        <div>{{msg}}</div>
    </div>
    <script src="js/vue.js"></script>
    <script>
        /*
            Vue的基本使用步骤
            1.需要提供标签用于填充数据
            2.引入 vue.js 库文件
            3.可以使用 vue 的语法做功能
            4.把vue提供的数据填充到标签里面 
        */
        var vm = new Vue({
            el: '#app',
            data: {
                msg: 'Hello Vue!'
            }
        })
    </script>
</body>
  • el:元素的挂载位置(值可以是 CSS 选择器或者 DOM 元素)
  • data:模型数据(值是一个对象)
  • {{msg}}:插值表达式
    • 将数据填充到 HTML 标签中
    • 插值表达式支持基本的计算操作

Vue 代码运行原理

  • 编译过程(Vue语法 -> 原生语法)

三、Vue 模板语法

3.1 模板语法概述

  1. 前端渲染

    把数据填充到 HTML 标签中。

  2. 前端渲染方式

    • 原生 js 拼接字符串

      var d = data.weather;
      var info = document.getElementById('info');
      info.innerHTML = '';
      for (var i = 0; i < d.length; i++) {
          var date = d[i].date;
          var day = d[i].info.day;
          var night = d[i].info.night;
          var tag = '';
          tag += '<span>日期:' + date + '</span><ul>';
          tag += '<li>白天天气:' + day[1] + '</li>';
          tag += '<li>白天温度:' + day[2] + '</li>';
          tag += '<li>白天风向:' + day[3] + '</li>';
          tag += '<li>白天风速:' + day[4] + '</li>';
          tag += '</ul>';
          var div = document.createElement('div');
          div.innerHTML = tag;
          info.appendChild(div);
      }
      
      • 缺点:不同开发人员的代码风格差别很大,随着业务的复杂,后期的维护变得越来越困难。
    • 使用前端模板引擎

      下面的代码是基于模板引擎 art-template 的代码,与拼接字符串相比,代码明显规范很多,它拥有自己的一套模板语法规则。

      <script id="abc" type="text/html">
          {{if isAdmin}}
          <h1>{{title}}</h1>
          <ul>
              {{each list as value i}}
                  <li>索引 {{i + 1}} :{{value}}</li>
              {{/each}}
          </ul>
          {{/if}}
      </script>
      
      • 优点:大家都遵循同样的规则编写代码,代码可读性明显提高,方便后期的维护
      • 缺点:没有专门提供事件机制
    • 使用 vue 特有的模板语法

      • 差值表达式
      • 指令
      • 事件绑定
      • 属性绑定
      • 样式绑定
      • 分支循环结构

3.2 指令

什么是指令?

指令的格式:以 v- 开始(比如 v-block)。

3.2.1 v-cloak
  • 插值表达式存在的问题:“闪动”,使用 v-cloak 指令解决
  • 解决原理:先隐藏,替换好值后再显示最终的值。
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>v-cloak</title>
  <style>
    [v-cloak] {
      display: none;
    }
  </style>
</head>
    
<body>
  <div id="app">
    <div v-cloak>{{msg}}</div>
  </div>
  <script src="js/vue.js"></script>
  <script>
    /*
      v-cloak指令的用法
      1、提供样式
        [v-cloak] {
          display: none;
        }
      2、在插值表达式所在的标签添加 v-cloak 指令
    
      原理:先通过样式隐藏内容,再在内存中进行值的替换,替换好之后再显示最终的结果
    */
    var vm = new Vue({
      el: '#app',
      data: {
        msg: 'hello Vue'
      }
    });
  </script>
</body>
3.2.2 数据绑定指令
  • v-text 填充纯文本
    • 相比插值表达式更加简洁
  • v-html 填充 HTML 片段
    • 存在安全问题
    • 本网站内部数据可以用使用,来自第三方的数据不可以用
  • v-pre 填充原始信息
    • 显示原始信息,跳过编译过程(分析编译过程)
<body>
    <div id="app">
        <!-- 原样输出 -->
        <div v-text="msg1"></div>
        <!-- 解析 html 标签 -->
        <div v-html="msg2"></div>
        <!-- 跳过编译输出 -->
        <div v-pre>{{msg1}}</div>
    </div>
    <script src="js/vue.js"></script>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                msg1: '<h1>v-text<h1>',
                msg2: '<h1>v-html<h1>'
            }
        });
    </script>
</body>
3.2.3 数据响应式
  • 如何理解响应式
    • html5 中的响应式(屏幕尺寸的变化导致样式的变化)
    • 数据的响应式(数据的变化导致页面内容的变化)
  • 什么是数据绑定
    • 数据绑定:将数据填充到标签中
  • v-once 只编译一次
    • 显示内容之后不再具有响应式功能
<body>
    <div id="app">
        <div v-once>{{msg}}</div>
    </div>
    <script src="js/vue.js"></script>
    <script>
        /*
            v-once的应用场景:如果显示的信息后续不需要再修改,就可以使用 v-once,提高性能。
        */
        var vm = new Vue({
            el: '#app',
            data: {
                msg: 'v-once'
            }
        });
    </script>
</body>
3.2.4 双向数据绑定
  • v-model 指令
<body>
    <div id="app">
        <div>{{msg}}</div>
        <input type="text" v-model="msg">
    </div>
    <script src="js/vue.js"></script>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                msg: 'v-model'
            }
        });
    </script>
</body>

MVVM 设计思想:分而治之

  • M(model)数据
  • V(view)视图,模板
  • VM(view-model)

注意:v-model 不是任何标签都可以设置的。

因为 span 标签不能在页面上修改内容,是不应该有双向绑定的,这里报错也是可以理解。

v-model 的底层实现原理:

<input type="text" v-bind:value="msg" v-on:input="msg=$event.target.value" />

使用 v-bind 绑定属性,在数据修改后,内容自动修改;使用 v-on 事件绑定,在输入域内容发生改变,修改数据模型。

<body>
    <div id="app">
        <div>{{msg}}</div>
        <!-- <input type="text" v-bind:value="msg" v-on:input="handle" /> -->
        <input type="text" v-bind:value="msg" v-on:input="msg=$event.target.value" />
    </div>
    <script src="js/vue.js"></script>
    <script>
        /*
            v-model 指令的本质
        */ 
        var vm = new Vue({
            el: '#app',
            data: {
                msg: 'hello'
            }
            // methods: {
            //     handle: function(event) {
            //         // 使用输入域中最新的数据覆盖原来的数据
            //         this.msg = event.target.value;
            //     }
            // }
        });
    </script>
</body>
3.2.5 事件绑定
  • v-on 指令

    <input type="button" v-on:click="num++" />
    

    简写形式:

    <input type="button" @click="num++" />
    
    <body>
        <div id="app">
            <div>{{num}}</div>
            <button v-on:click="num++">点击</button>
        </div>
    
        <script src="js/vue.js"></script>
        <script>
            /* 事件绑定 */
            var vm = new Vue({
                el: '#app',
                data: {
                    num: 0
                }
            })
        </script>
    </body>
    
  • 事件函数的调用方式

    • 直接绑定函数名称

      <button v-on:click="say">点击</button>
      
    • 调用函数

      <button v-on:click="say()">点击</button>
      
    <script>
        /* 事件绑定 */
        var vm = new Vue({
            el: '#app',
            data: {
                num: 0
            },
            methods: {
                say: function() {
                    // 这里的 this 是 Vue 的实例对象
                    this.num++;
                }
            }
        })
    </script>
    
  • 事件函数参数传递

    • 如果事件直接绑定函数名称,那么默认会传递事件对象作为事件函数的第一个参数;

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

      <button v-on:click="say('hi', $event)">Say hi</button>
      
  • 事件修饰符

    • .stop 阻止冒泡

      <a v-on:click.stop="handle">跳转</a>
      
    • .prevent 阻止默认行为

      <a v-on:click.prevent="handle">跳转</a>
      
  • 按键修饰符

    • .enter 按下回车键

      <input type="text" v-on:keyup.enter="submit"/>
      
    • .delete 按下删除键

      <input type="text" v-on:keyup.delete="delete"/>
      
  • 自定义按键修饰符

    • 全局 config.keyCodes 对象
    <body>
        <div id="app">
            <input type="text" v-on:keyup.aaa="handle" v-model="info" />
        </div>
        <script src="js/vue.js"></script>
        <script>
            /*
                事件绑定-自定义按键修饰符
                规则:自定义按键修饰符名字是自定义的,但是对应的值必须是按键对应 event.keyCode 值
            */
           Vue.config.keyCodes.aaa = 65;
           var vm = new Vue({
                el: '#app',
                data: {
                    info: ''
                },
                methods: {
                    handle: function(event) {
                        console.log(event.keyCode);
                    }
                }
           });
        </script>
    </body>
    
3.2.6 属性绑定
  • v-bind 指令

    <a v-bind:href='url'>跳转</a>
    
  • 缩写形式

    <a :href='url'>跳转</a>
    
<body>
    <div id="app">
        <a v-bind:href="url" v-text='title'></a>
        <a :href="url2">简写</a>
        <button v-on:click="handle">切换</button>
    </div> 
    <script src="js/vue.js"></script>
    <script>
        /* 属性绑定  */
        var vm = new Vue({
            el: '#app',
            data: {
                title: '百度',
                url: 'https://www.baidu.com',
                url2: 'https://www.baidu.com'
            },
            methods: {
                handle: function(){
                    // 修改 url
                    this.title = '新浪';
                    this.url = 'https://www.sina.com';
                }
            }
        });
    </script>
</body>
3.2.7 样式绑定
  1. class 样式处理

    • 对象语法

      <div v-bind:class="{ active: isActive }"></div>
      
    • 数组语法

      <div v-bind:class="{ activeClass, errorClass }"></div>
      
    <body>
        <div id="app">
            <div v-bind:class="{ active: isActive, error: isError }">文字</div>
            <!-- <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: {
                    isActive: true,
                    isError: true,
                    activeClass: 'active',
                    errorClass: 'error'
                },
                methods: {
                    handle: function() {
                        this.isActive = !this.isActive;
                        this.isError = !this.isError;
                        this.activeClass = this.activeClass == '' ? 'active' : '';
                        this.errorClass = this.errorClass == '' ? 'error' : '';
                    }  
                }
           });
        </script>
    </body>
    

    对象语法和数组语法可以结合使用。

    <div v-bind:class="[activeClass, { error: isError }]">文字</div>
    

    class 可以简化操作:

    <div v-bind:class="arrClasses">文字</div>
    <div v-bind:class="objClasses">文字</div>
    
    <script>
        var vm = new Vue({
            data: {
                arrClasses: ['active', 'error'],
                objClasses: { active: true, error: true }
            }
        });
    </script> 
    

    标签默认的样式会保留:

    <div class="base" v-bind:class='[ activeClass, errorClass ]'>文字</div>
    
  2. style 样式处理

    • 对象语法

      <div v-bind:style="{ color: activeColor, fontSize: fontSize }"></div>
      
    • 数组语法

      <div v-bind:style="{ baseStyles, overridingStyles }"></div>
      

3.3 分支循环结构

  1. 分支结构

    • v-if
    • v-else
    • v-else-if
    • v-show
    <body>
        <div id="app">
            <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'>show</div>
            <button v-on:click="handle">点击</button>
        </div>
        <script src="js/vue.js"></script>
        <script>
            /* 分支结构 
                
                v-show 的原理:控制元素样式是否显示 display: none
            */
           var vm = new Vue({
                el: '#app',
                data: {
                    score: 88,
                    flag: false
                },
                methods: {
                    handle: function() {
                        this.flag = !this.flag;
                    }
                }
           });
        </script>
    </body>
    

    v-if 与 v-show 的区别:

    • v-if 控制元素是否渲染到页面
    • v-show 控制元素是否显示(已经渲染到了页面上)
  2. 循环结构

    • v-for 遍历数组

      <li v-for="item in list">{{item}}</li>
      <li v-for="(item, index) in list">{{item + '---' + index}}</li>
      
    • key 的作用:帮助 Vue 区分不同的元素,从而提高性能

      <li :key="item.id" v-for="(item, index) in list">{{item + '---' + index}}</li>
      
    • v-for 遍历对象

      <div v-for="(value, key, index) in object"></div>
      
    • v-if 和 v-for 结合使用

      <div v-if="value==12" v-for="(value, key, index) in object"></div>
      
    <body>
        <div id="app">
            <div>水果列表</div>
            <ul>
                <li v-for="item in fruits">{{item}}</li>
                <li v-for="(item, index) in fruits">{{item + '---' + index}}</li>
                
                <li :key="item.id" v-for="(item, index) in myFruits">
                    <span>{{item.ename}}</span>
                    <span>---</span>
                    <span>{{item.cname}}</span>
                </li>
            </ul>
        </div>
        <script src="js/vue.js"></script>
        <script>
            /*
                循环结构
            */
           var vm = new Vue({
                el: '#app',
                data: {
                    fruits: [ 'apple', 'orange', 'banana' ],
                    myFruits: [{
                        id: 1,
                        ename: 'apple',
                        cname: '苹果'
                    }, {
                        id: 2,
                        ename: 'orange',
                        cname: '橙子'
                    }, {
                        id: 3, 
                        ename: 'banana',
                        cname: '香蕉'
                    }]
                }
           });
        </script>
    </body>
    

四、Vue 常用特性

4.1 常用特性概览

  • 表单操作
  • 自定义指令
  • 计算属性
  • 过滤器
  • 侦听器
  • 生命周期

4.2 表单操作

  1. 基于 Vue 的表单操作

    • input 单行文本
    • textarea 多行文本
    • select 下拉多选
    • radio 单选框
    • checkbox 多选框
    <body>
        <div id="app">
            <form>
                <div>
                    <span>姓名:</span>
                    <span>
                        <input type="text" v-model="username">
                    </span>
                </div>
                <div>
                    <span>性别:</span>
                    <span>
                        <input type="radio" id="man" value="1" v-model="gender">
                        <label for="man">男</label>
                        <input type="radio" id="woman" value="2" v-model="gender">
                        <label for="woman">女</label>
                    </span>
                </div>
                <div>
                    <span>爱好:</span>
                    <input type="checkbox" name="" id="ball" value="1" v-model="hobby">
                    <label for="ball">篮球</label>
                    <input type="checkbox" name="" id="sing" value="2" v-model="hobby">
                    <label for="sing">唱歌</label>
                    <input type="checkbox" name="" id="code" value="3" v-model="hobby">
                    <label for="code">写代码</label>
                </div>
                <div>
                    <span>职业:</span>
                    <select v-model="job">  
                        <option value="0">请选择职业</option>
                        <option value="1">教师</option>
                        <option value="2">软件工程师</option>
                        <option value="3">律师</option>
                    </select>
                </div>
                <div>   
                    <span>个人简介:</span>
                    <textarea v-model="desc"></textarea>
                </div>
                <div>
                    <input type="submit" value="提交" v-on:click.prevent="handle">
                </div>
            </form>
        </div>
    
        <script src="js/vue.js"></script>
        <script>
            /*
                表单基本操作
            */
           var vm = new Vue({
               el: '#app',
               data: {
                   username: '',
                   gender: 2,
                   hobby: ['1', '2'],
                   job: 0,
                   desc: ''
               },
               methods: {
                   handle: function() {
                       console.log(this.username);
                       console.log(this.gender);
                       console.log(this.hobby.toString());
                       console.log(this.job);
                       console.log(this.desc);
                   }
               }
           });
        </script>
    </body>
    
  2. 表单域修饰符

    • number:转换为数值
    • trim:去掉开始和结尾的空格
    • lazy:将 input 事件切换为 change 事件
    <body>
        <div id="app">
            <input type="text" v-model.number="age">
            <input type="text" v-model.trim="info">
            <!-- input 事件每次修改内容时触发,change 事件在失去焦点时触发 -->
            <input type="text" v-model.lazy="msg">
            <div>{{msg}}</div>
            <button @click="handle">点击</button>
        </div>
    
        <script src="js/vue.js"></script>
        <script>
            var vm = new Vue({
                el: "#app",
                data: {
                    age: '',
                    info: '',
                    msg: ''
                },
                methods: {
                    handle: function() {
                        console.log(this.age + 12);
                        console.log('info.length = ' + this.info.length);
                    }   
                }
            });
        </script>
    </body>
    

4.3 自定义指令

内置指令不满足需求时使用。

  1. 自定义指令的语法规则(获取元素焦点)

    Vue.directive('focus', {
        // el 表示指令所绑定的元素
        inserted: function(el) {
            // 获取元素的焦点
            el.focus();
        }
    })
    

    用法:

    <body>
        <div id="app">
            <input type="text" v-focus>
        </div>
    
        <script src="js/vue.js"></script>
        <script>
            /*
                自定义指令
            */
            Vue.directive('focus', {
                inserted: function(el) {
                   // el 表示指令所绑定的元素
                   el.focus();
                } 
            });
            var vm = new Vue({
                el: '#app'
            });
        </script>
    </body>
    
  2. 带从参数的自定义指令(改变元素背景色)

    Vue.directive('color', {
        inserted: function(el, binding) {
            el.style.backgroundColor = binding.value.color;
        }
    });
    

    用法:

    <input type="text" v-color="{color: 'orange'}" />
    
  3. 局部指令

    var vm = new Vue({
        el: '#app',
        data: {
        
        },
        directives: {
            focus: {
                // 指令的定义
                inserted: function(el) {
                    el.focus();
                }
            },
            color: {
                bind: function(el, binding) {
                    el.style.backgroundColor = binding.value.color; 
                }
            }
        }
    })
    

4.4 计算属性

表达式的计算逻辑可能会比较复杂,使用计算属性可以使模板内容更加简洁。

  1. 计算属性的用法

    <body>
        <div id="app">
            <div>{{msg}}</div>
            <div>{{reversedMessage}}</div>
        </div>
    
        <script src="js/vue.js"></script>
        <script>
            /*
                计算属性
            */
           var vm = new Vue({
                el: '#app',
                data: {
                    msg: 'Nihao'
                },
                computed: {
                    reversedMessage: function() {
                        return this.msg.split('').reverse().join('');
                    }
                }
           });
        </script>
    </body>
    
  2. 计算属性和方法的区别

    • 计算属性是基于它们的依赖进行缓存的
    • 方法不进行缓存
    <body>
        <div id="app">
            <div>{{msg}}</div>
            <div>{{reverseMessage}}</div>
            <div>{{reverseMessage}}</div>
            <div>{{reverseMessage}}</div>
            <div>{{reverseString()}}</div>
            <div>{{reverseString()}}</div>
            <div>{{reverseString()}}</div>
        </div>
    
        <script src="js/vue.js"></script>
        <script>
            /*
                计算属性
            */
           var vm = new Vue({
                el: '#app',
                data: {
                    msg: 'Nihao'
                },
                methods: {
                    reverseString: function() {
                        console.log('reverseString run!');
                        return this.msg.split('').reverse().join('');
                    }
                },
                computed: {
                    reverseMessage: function() {
                        console.log('reverseMessage run!');
                        return this.msg.split('').reverse().join('');
                    }
                }
           });
        </script>
    </body>
    

    方法执行了三次,而计算属性在 msg 未改变的情况下只执行了一次,优化性能。

4.5 侦听器

  1. 侦听器的应用场景

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

  2. 侦听器的用法

    <body>
        <div id="app">
            <div>
                <span>名:</span>
                <span>
                    <input type="text" v-model="firstName" />
                </span>
            </div>
            <div>
                <span>姓:</span>
                <span>
                    <input type="text" v-model="lastName" />
                </span>
            </div>
            <div>{{fullName}}</div>
        </div>
    
        <script src="js/vue.js"></script>
        <script>
            /*
                侦听器
            */
           var vm = new Vue({
                el: '#app',
                data: {
                    firstName: 'Lebron',
                    lastName: 'James',
                    fullName: 'Lebron James'
                },
                watch: {
                    // firstName 与上面的数据名相同
                    firstName: function(val) {
                        // val 表示变化之后的值
                        this.fullName = val + ' ' + this.lastName;
                    },
                    lastName: function(val) {
                        this.fullName = this.firstName + ' ' + val;
                    }
                }
           });
        </script>
    </body>
    

4.6 过滤器

  1. 过滤器的作用

    格式化数据,比如将字符串格式化为首字母大写,将日期格式化为指定的格式等。

  2. 自定义过滤器

    Vue.filter('过滤器名称', function(value) {
        // 过滤器业务逻辑
    });
    

    使用:

    <div>{{msg | upper}}</div>
    <div>{{msg | upper | lower}}</div>
    <div v-bind:id="id | formatId"></div>
    
  3. 局部过滤器

    var vm = new Vue({
        filters: {
            upper: function(value) {
                return value.chatAt(0).toUpperCase() + value.slice(1);
            }
        }
    });
    
    <body>
        <div id="app">
            <input type="text" v-model="msg"/>
            <div>{{msg | upper}}</div>
            <div>{{msg | lower}}</div>
            <!-- 级联操作,先进行首字母大写,再进行首字母小写 -->
            <div>{{msg | upper | lower}}</div>
        </div>
    
        <script src="js/vue.js"></script>
        <script>
            /*
                过滤器
            */ 
           // 首字母大写
           Vue.filter('upper', function(value) {
                return value.charAt(0).toUpperCase() + value.slice(1);
           });
           // 首字母小写
           Vue.filter('lower', function(value) {
               return value.charAt(0).toLowerCase() + value.slice(1);
           })
           var vm = new Vue({
                el: '#app',
                data: {
                    msg: ''
                },
                // 局部过滤器
                // filters: {
                //     upper: function(value) {
                //         return value.charAt(0).toUpperCase + value.slice(1);
                //     }
                // }
           });
        </script>
    </body>
    
  4. 带参数的过滤器

    Vue.filter('format', function(value, arg) {
        // arg 就是过滤器传递过来的参数
    });
    

    使用:

    <div>{{date | format('yyyy-MM-dd')}}</div>
    

4.7 生命周期

  1. 主要阶段
    • 挂载(初始化相关属性)
      • beforeCreate:在 Vue 实例初始化之后,数据观测和事件配置之前被调用
      • created:在 Vue 实例创建完成后被立即调用
      • beforeMount:在挂载开始之前被调用
      • mounted:el 被新创建的 vm.$el 替换,并挂载到 Vue 实例上之后调用该钩子
    • 更新(元素或组件的变更操作)
      • beforeUpdate:数据更新时调用,发生在虚拟 DOM 打补丁之前
      • updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子
    • 销毁(销毁相关属性)
      • beforeDestory:Vue 实例销毁之前调用
      • destoryed:Vue 实例销毁后调用

<body>
    <div id="app">
        <div>{{msg}}</div>
        <button @click="update">更新</button>
        <button @click="destroy">销毁</button>
    </div>

    <script src="js/vue.js"></script>
    <script>
        /*
            vue 生命周期
        */
       var vm = new Vue({
         el: '#app',
         data: {
             msg: '生命周期'
         },
         methods: {
            update: function() {
                this.msg = '更新';
            },
            destroy: function() {
                this.$destroy();
            },
         },
         beforeCreate: function() {
             console.log('beforeCreate');
         },
         created: function() {
            console.log('created');
         },
         beforeMount: function() {
             console.log('beforeMount');
         },
         mounted: function() {
             console.log('mounted');
         },
         beforeUpdate: function() {
             console.log('beforeUpdate');
         },
         updated: function() {
             console.log('updated');
         },
         beforeDestroy: function() {
             console.log('beforeDestroy');
         },
         destroyed: function() {
             console.log('destroyed');
         }
       })
    </script>
</body>

五、数组响应式变化

  • Vue.set(vm.items, indexOfItem, newValue)
  • vm.$set(vm.items, indexOfItem, newValue)
    • 参数 1:表示要处理的数组名称
    • 参数 2:表示要处理的数组的索引
    • 参数 3:表示要处理的数组的值
var vm = new Vue({
    el: '#app',
    data: {
        list: ['apple', 'orange', 'banana'],
        info: {
            name: 'Tom'
        }
    }
});

//vm.list[1] = 'lemon';  // 不是响应式的
//Vue.set(vm.list, 2, 'lemon');
vm.$set(vm.list, 2, 'lemon');

// 也可以处理对象
//vm.info.gender = 'man';  // 不是响应式的
vm.$set(vm.info, 'gender', 'man'); // 随后再修改 gender 值会同步更新页面

六、组件化开发

6.1 组件化开发思想

  • 标准
  • 分治
  • 重用
  • 组合

不同的功能封装到不同的组件中,然后组件通过组合的方式形式一个完整的应用。

组件化规范:Web Components

自己实现组件时的问题:

  • 尽可能多的重用代码
  • 自定义组件的方式不太容易(html、css 和 js)
  • 多次使用组件可能导致冲突

Web Components 通过创建封装好功能的定制元素解决上述问题。

Vue 部分实现了此规范。

6.2 组件注册

  1. 全局组件注册语法

    Vue.component(组件名称, {
        data: 组件数据,
        template: 组件模板内容
    })
    
    // 定义一个名为 button-counter 的新组件
    Vue.component('button-counter', {
        data: function() {
            return {
                count: 0
            }
        },
        template: '<buttton v-on:click="count++">点击了{{count}}次。</button>'
    })
    
  2. 组件用法

    <div id="app">
        <button-counter></button-counter>
    </div>
    

    完整示例:

    <body>
        <div id="app">
            <button-counter></button-counter>
            <button-counter></button-counter>
            <button-counter></button-counter>
        </div>
        
        <script src="js/vue.js"></script>
        <script>
            /*
                组件注册
            */
            Vue.component('button-counter', {
                data: function () {
                    return {
                        count: 0
                    }
                },
                template: '<button v-on:click="handle">点击了{{count}}次</button>',
                methods: {
                    handle: function () {
                        this.count += 2;
                    }
                },
            })
            var vm = new Vue({
                el: '#app',
                data: {
        
                }
            })
        </script>
    </body>
    
  3. 组件注册注意事项

    • data 必须是一个函数

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

      template: '<button v-on:click="handle">点击了{{count}}次</button><button>测试</button>',
      

      只有两个同级的 button 按钮,没有统一的父元素,将会报错。

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

      • 模板字符串需要浏览器提供支持(ES6语法)
      • 在模板内容很长时,使用模板字符串提高可读性
      template: `
          <div>
              <button v-on:click="count++">点击了{{count}}次</button>
              <button>测试</button>
          </div>
      `,
      
    • 组件命名方式

      • 短横线方式 Vue.component('my-component', {})
      • 驼峰方式 Vue.component('MyComponent', {})
  4. 局部组件注册

    var ComponentA  = {
        data = function() {
            return {
                msg: ''
            }
        },
        template: '<div>{{msg}}</div>'
    }
    var ComponentB  = { ... }
    var ComponentC  = { ... }
    new Vue({
        el: '#app',
        components: {
            'component-a':  ComponentA,
            'component-b':  ComponentB,
            'component-c':  ComponentC,
        }
    })
    

    局部组件只能在注册它的父组件中使用。

6.3 组件间数据交互

6.3.1 父组件向子组件传值
  1. 组件内部通过 props 接收传递过来的值

    Vue.component('menu-item', {
        props: ['title'],
        template: '<div>{{ title }}</div>'
    })
    
  2. 父组件通过属性将值传递给子组件

    <menu-item title="来自父组件的数据"></menu-item>
    <menu-item :title="title"></menu-item> <!--动态绑定属性值-->
    
  3. props 属性名规则

    • 在 props 中使用驼峰形式,模板中需要使用短横线的形式
    • 字符串形式的模板中没有这个限制
    <!-- 在 html 中短横线形式的 -->
    <menu-item  menu-title="nihao"></menu-item>
    
    <script>
        Vue.component('menu-item', {
            // 在 JavaScript 中是驼峰形式
            props: ['menuTitle'],
            template: '<div>{{ menuTitle }}</div>'
        })
    </script>
    
  4. props 属性值类型

    • 字符串 String
    • 数值 Number
    • 布尔值 Boolean
    • 数组 Array
    • 对象 Object
    <div id="app">
        <!-- pnum 是字符串类型,pbool 是字符串类型 -->
        <menu-item :pstr='pstr' pnum='12' pbool='true'></menu-item>
        <!-- pnum 是数值类型,pbool 是布尔类型 -->
        <menu-item :pstr='pstr' :pnum='12' :pbool='true' :parr='parr' :pobj='pobj'></menu-item>
    </div>
    
    <script>
        Vue.component('menu-item', {
            props: ['pstr', 'pnum', 'pbool', 'parr', 'pobj'],
            template: `
                <div>
                    <div>{{ pstr }}</div>
                    <div>{{ typeof pnum }}</div>
                    <div>{{ typeof pbool }}</div>
                    <ul>
                        <li :key='index' v-for='(item, index) in parr'>{{ item }}</li>
                    </ul>
                    <div>
                        <span>{{ pobj.msg }}</span>
                    <div>
                </div>
            `
        });
        var vm = new Vue({
            el: '#app',
            data: {
                pstr: 'hello',
                parr: ['apple', 'orange'],
                pobj: {
                    msg: ''
                }
            }
        })
    </script>
    
6.3.2 子组件向父组件传值
  1. props 传递数据原则:单向数据流。只允许父组件向子组件传递数据,而不允许子组件操作 props 中的数据。

    <body>
        <div id="app">
            <menu-item :parr="parr"></menu-item>
        </div>
        
        <script>
            /* 能够修改 parr 的值,从而影响父组件的内容,但是不推荐这样做  */
            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>
                    </div>
                `  
            });
            var vm = new Vue({
                el: '#app',
                data: {
                    pmsg: '父组件中的内容',
                    parr: ['apple', 'orange']
                }
            })
        </script>
    </body>
    
  2. 子组件通过自定义事件向父组件传递信息

    <button v-on:click='$emit("enlarge-text", 0.1)'>放大字体</button>
    
  3. 父组件监听子组件的事件

    <menu-item v-on:enlarge-text="fontSize += $event"></menu-item>
    <menu-item @enlarge-text="fontSize += $event"></menu-item>
    
6.3.3 非父子组件间传值
  1. 单独的事件中心管理组件间的通信

    var eventHub = new Vue();
    
  2. 监听事件与销毁事件

    eventHub.$on('add-todo', addTodo);
    eventHub.$off('add-todo');
    
  3. 触发事件

    eventHub.$emit('add-todo', id);
    

示例代码:

<body>
    <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>
</body>

6.4 组件插槽

6.4.1 组件插槽的作用
  • 父组件向子组件传递内容

6.4.2 组件插槽基本用法
  1. 插槽位置

    Vue.component('alert-box', {
        template: `
            <div class="demo-alert-box">
                <strong>Error!</strong>
                <slot></slot>
            </div>
        `
    })
    
  2. 插槽内容

    <alert-box>Something bad happened.</alert-box>
    

    示例代码:

    <body>
        <div id="app">
            <alert-box>有bug发生</alert-box>
            <alert-box></alert-box>
        </div>
    
        <script src="js/vue.js"></script>
        <script>
            /* 
                组件插槽 
            */
            Vue.component('alert-box', {
                template: `
                    <div>
                        <strong>ERROR:</strong>
                        <slot>默认内容</slot>
                    </div>
                `
            });
            var vm = new Vue({
                el: '#app',
                data: {
    
                }
            })
        </script>
    </body>
    
  3. 具名插槽

    • 插槽定义

      Vue.component('base-layout',{
          template: `
              <div class="container">
                  <header>
                      <slot name="header"></slot>
                  </header>
                  <main>
                      <slot></slot>
                  </main>
                  <footer>
                      <slot name="footer"></slot>
                  </footer>
              </div>
          `
      });
      
    • 插槽内容

      <base-layout>
          <template slot="header">
              <p>标题内容1</p>
              <p>标题内容2</p>
          </template>
          <p>主要内容1</p>
          <p>主要内容2</p>
          <template slot="footer">
              <p>底部内容1</p>
              <p>底部内容2</p>
          </template>
      </base-layout>
      
  4. 作用域插槽

    • 应用场景:父组件对子组件的内容进行加工处理。
    • 插槽定义
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>作用域插槽</title>
        <style>
            .current {
                color: orange;
            }
        </style>
    </head>
    
    <body>
        <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: `
                  <ul>
                    <li :key="item.id" v-for="(item, index) in list">
                        <slot :info='item'>{{item.name}}</slot>    
                    </li>
                 </ul>
               `
            })
    
            var vm = new Vue({
                el: '#app',
                data: {
                    list: [{
                        id: 1,
                        name: 'apple'
                    }, {
                        id: 2,
                        name: 'orange'
                    }, {
                        id: 3,
                        name: 'banana'
                    }]
                }
            })
        </script>
    </body>
    

七、前后端交互

7.1 前后端交互模式

  • 原生 ajax
  • 基于 jQuery 的 ajax
  • fetch
  • axios

7.2 Promise 用法

  1. 异步调用
    • 定时任务
    • Ajax
    • 事件函数
  2. 多次异步调用的依赖分析
    • 多次异步调用的结果顺序不确定
    • 异步调用结果如果尺寸在依赖需要嵌套

Promise 是异步编程的一种解决方案,从语法上来讲,Promise 是一个对象,从它可以获取异步操作的消息。

使用 promise 主要有以下好处:

  • 可以避免多层异步调用嵌套问题(回调地狱)
  • Promise 对象提供了简洁的 API,使得控制异步操作更加容易。

使用步骤:

  • 实例化 Promise 对象,构造函数中传递函数,该函数中用于处理异步任务
  • resolve 和 reject 两个参数用于处理成功和失败两种情况,并通过 p.then 获取处理结果。
var p = new Promise(function(resolve, reject) {
    // 成功时调用 resolve()
    // 失败时调用 reject()
});
p.then(function(ret) {
    // 从 resolve 得到正常数据  
}, function(ret) {
    // 从 reject 得到错误信息
});

基于 Promise 处理 Ajax 请求:

  1. 处理原生 Ajax

    function queryData(url) {
        return new Promise(function(resolve, reject) {
            var xhr = new XMLHttpRequest();
            xhr.onreadystatechange = function() {
                if (xhr.readyState != 4) 
                    return;
                if (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);
            return queryData('http://localhost:3000/data1');
        }, function(info) {
            console.log(info);
        })
        .then(function(data) {
            console.log(data);
            return queryData('http://localhost:3000/data2')
        })
        .then(function(data) {
            console.log(data);
        });
    

then 参数中的函数返回值:

  1. 返回 Promise 实例对象
    • 返回的该实例对象会调用下一个 then
  2. 返回普通值
    • 返回的普通值会直接传递给下一个 then,通过 then 参数中函数的参数接收该值
    • 原因:会产生一个默认的 Promise 对象来调用下一个 then

Promise 常用的 API:

  1. 实例方法

    • p.then() 得到异步任务的正确结果
    • p.catch() 获得异步的结果
    • p.finally() 成功与否都会执行
    queryData().then(function(data) {
        console.log(data);
    })
    .catch(function(data) {
        console.log(data);
    })
    .finally(function() {
        console.log('finished');
    })
    
  2. 对象方法

    • Promise.all() 并发处理多个异步任务,所有任务都执行完成才能得到结果
    • Promise.race() 并发处理多个异步任务,只要有一个任务完成就能得到结果
    Promise.all([p1, p2, p3]).then((result) => {
        console.log(result);
    })
    
    Promise.race([p1, p2, p3]).then((result) => {
        console.log(result);
    })
    

7.3 接口调用 -fetch 用法

  1. 基本特性

    • 更加简单的数据获取方式,功能更强大、更灵活,可以看作是 xhr 的升级版
    • 基于 Promise 实现
  2. 语法结构

    fetch('/abc').then(data => {
        // text() 方法属于 fetchAPI 的一部分,它返回一个 Promise 实例对象,用于获取后台返回的数据
        return data.text();
    }).then(ret => {
        // 这里才是最终的数据
        console.log(ret);
    }).catch(function(data) {
        
    });
    
  3. fetch 请求参数

    1. 常用配置选项

      • method(String):HTTP 请求方法,默认为 GET
      • body(String):HTTP 的请求参数
      • headers(Object):HTTP 的请求头,默认为 {}
      fetch('/abc', {
          method: 'get'
      }).then(data => {
          return data.text();
      }).then(ret => {
          // 这里才是最终的数据
          console.log(ret);
      });
      
    2. GET 请求方式的参数传递

      fetch('/abc?id=123').then(data => {
          return data.text();
      }).then(ret => {
          // 这里才是最终的数据
          console.log(ret);
      });
      

      Restful 风格:

      fetch('/abc/123', {
          method: 'get'
      }).then(data => {
          return data.text();
      }).then(ret => {
          // 这里才是最终的数据
          console.log(ret);
      });
      
    3. DELETE 请求方式的参数传递与 GET 相似。

    4. POST 请求方式的参数传递

      fetch('/books', {
          method: 'post',
          body: 'username=Tom&age=13',
          headers: {
              'Content-Type': 'application/x-www-form-urlencoded'
          }
      }).then(data => {
          return data.text();
      }).then(ret => {
          console.log(ret);
      })
      
      fetch('/books', {
          method: 'post',
          body: JSON.stringify({
              username: 'Tom',
              age: 13
          }),
          headers: {
              'Content-Type': 'application/json'
          }
      }).then(data => {
          return data.text();
      }).then(ret => {
          console.log(ret);
      })
      
    5. PUT 请求方式的参数传递与 POST 相似。

  4. fetch 响应结果

    响应数据格式

    • text():将返回体处理成字符串类型
    • json():返回结果和 JSON.parse(responseText) 一样
    fetch('/abc').then(data => {
        return data.json();
    }).then(ret => {
        console.log(ret);
    });
    

7.4 接口调用 -axios 用法

  1. axios 的基本特性

    axios 是一个基于 Promise 用于浏览器和 node.js 的 HTTP 客户端。

    它具有以下特征:

    • 支持浏览器和 node.js
    • 支持 promise
    • 能拦截请求和响应
    • 自动转换 JSON 数据
  2. axios 的基本用法

    axios.get('/data').then(ret => {
        // data 属性名称是固定的,用于获取后台响应的数据
        console.log(ret.data);
    })
    
  3. axios 的常用 API

    • get:查询数据
    • post:添加数据
    • put:修改数据
    • delete:删除数据
  4. axios 的参数传递

    • 通过 url 传递参数

      axios.get('http://localhost:3000/axios?id=123').then(function(ret) {
          console.log(ret.data);
      })
      axios.get('http://localhost:3000/axios/123').then(function(ret) {
          console.log(ret.data);
      })
      
    • 通过 params 选项传递参数

      // GET
      axios.get('http://localhost:3000/axios', {
          params: {
              id: 123
          }
      }).then(function(ret) {
          console.log(ret.data);
      })
      // POST
      axios.post('http://localhost:3000/axios', {
          username: 'Tom', 
          password: 123456
      }).then(function(ret) {
          console.log(ret.data);
      })
      // PUT
      axios.put('http://localhost:3000/axios/123', {
          username: 'Tom', 
          password: 123456
      }).then(function(ret) {
          console.log(ret.data);
      })
      
    • 通过 URLSearchParams 传递参数(application/x-www-form-urlencoded)

      const params = new URLSearchParams();
      params.append('username', 'Tom');
      params.append('password', '123456');
      axios.post('http://localhost:3000/axios', params).then(ret => {
          console.log(ret.data);
      })
      
  5. axios 的响应结果

    响应结果的主要属性:

    • data:实际响应回来的数据
    • headers:响应头信息
    • status:响应状态码
    • statusText:响应状态信息
  6. 全局配置

    • axios.defaults.timeout = 3000; 超时时间
    • axios.defaults.baseURL = 'http://localhost:3000/app'; 默认地址
    • axios.defaults.headers['mytoken'] = 'aqweeqweqewqew23a12w2'; 设置请求头
  7. axios 拦截器

    1. 请求拦截器

      在请求发出之前设置一些信息。

      // 添加一个请求拦截器
      axios.interceptors.request.use(function(config) {
          // 在请求发出之前进行一些信息设置
          config.headers.mytoken = 'nihao';
          return config;
      }, function(error) {
          console.log(error);
      })
      
    2. 响应拦截器

      在获取数据之前对数据做一些加工处理。

      // 添加一个响应拦截器
      axios.interceptors.response.use(function(res) {
          console.log(res);
          // data 是实际的数据
          var data = res.data;
          return data;
      }, function(error) {
           console.log(error);
      })
      

7.5 接口调用 async/await 用法

  • async/await 是 ES7 引入的新语法,可以更加方便的进行异步操作
  • async 关键字用于函数上(async 函数的返回值是 Promise 实例对象)
  • await 关键字用于 async 函数当中(await 可以得到异步的结果)
async function queryData(id) {
    const rsp = await axios.get('/data');
    return rsp; 
}
queryData.then(rsp => {
    console.log(rsp);
})

八、Vue 前端路由

8.1 路由

路由是一个比较广义和抽象的概念,路由的本质就是对应关系

在开发中,路由分为:

  • 后端路由
  • 前端路由
  1. 后端路由

    • 概念:根据不同的用户 URL 请求,返回不同的内容
    • 本质:URL 请求地址与服务器资源之间的对应关系

  2. SPA(Single Page Application)

    • 后端渲染(存在性能问题)
    • Ajax 前端渲染(前端渲染提高性能,但是不支持浏览器的前进后退操作)
    • SPA(Single Page Application)单页面应用程序:整个网站只有一个页面,内容的变化通过 Ajax 局部更新实现,同时支持浏览器地址栏的前进和后退操作
    • SPA 实现原理之一:基于 URL 地址的 hash(hash 的变化会导致浏览器记录访问历史的变化,但 hash 的变化不会触发新的 URL 请求)
    • 在实现 SPA 过程中,最核心的技术点是前端路由
  3. 前端路由

    • 概念:根据不同的用户事件,显示不同的页面内容。
    • 本质:用户事件事件处理函数之间的对应关系

  4. 实现简易前端路由

    • 基于 URL 中的 hash 实现(点击菜单的时候改变 URL 的 hash,根据 hash 的变化控制组件的切换)
    // 监听 window 的 onhashchange 事件,根据获取到的最新 hash 值,切换要显示的组件的名称
    window.onhashchange = function() {
        // 通过 location.hash 获取到最新的 hash 值
    }
    
    <body>
        <div id="app">
            <a href="#/a">页面A</a>
            <a href="#/b">页面B</a>
            <a href="#/c">页面C</a>
            <a href="#/d">页面D</a>
    
            <component :is="componentName"></component>
        </div>
    
        <script src="js/vue.js"></script>
        <script>
            const PageA = {
                template: `<h1>页面A</h1>`
            }
            const PageB = {
                template: `<h1>页面B</h1>`
            }
            const PageC = {
                template: `<h1>页面C</h1>`
            }
            const PageD = {
                template: `<h1>页面D</h1>`
            }
            /*
                简易前端路由
            */
           var vm = new Vue({
                el: '#app',
                data: {
                    componentName: 'pageA'
                },
                // 注册私有组件
                components: {
                    pageA: PageA,
                    pageB: PageB,
                    pageC: PageC,
                    pageD: PageD
                }
           })
    
           window.onhashchange = function() {
               console.log(location.hash.slice(1));
               switch(location.hash.slice(1)) {
                    case '/a': 
                        vm.componentName = 'pageA';
                        break;                
                    case '/b': 
                        vm.componentName = 'pageB';
                        break;                
                    case '/c': 
                        vm.componentName = 'pageC';
                        break;                
                    case '/d': 
                        vm.componentName = 'pageD';
                        break;
               }
           }
        </script>
    </body>
    

8.2 Vue Router

Vue Router(官网:https://router.vuejs.org/zh/) 是 Vue.js 官方的路由管理器。

它和 Vue.js 的核心深度集成,可以非常方便的用于 SPA 应用程序的开发。

Vue Router 包含的功能有:

  • 支持 HTML 历史模式或 hash 模式
  • 支持嵌套路由
  • 支持路由参数
  • 支持编程式路由
  • 支持命名路由
8.2.1 基本使用步骤
  1. 引入相关的库文件

    <!-- 导入 vue 文件,为全局 window 对象挂载 Vue 构造函数 -->
    <script src="js/vue.js"></script>
    <!-- 导入 vue-router 文件,为全局 window 对象挂载 VueRouter 构造函数 -->
    <script src="js/vue-router_3.0.2.js"></script>
    
  2. 添加路由链接

    <!-- router-link 是 vue 中提供的标签,默认会被渲染为 a 标签 -->
    <!-- to 属性默认会被渲染为 href 属性 -->
    <!-- to 属性的值默认会被渲染为 # 开头的 hash 地址 -->
    <router-link to="/user">User</router-link> 
    <router-link to="/register">Register</router-link>   
    
  3. 添加路由填充位

    <!-- 路由填充位(也叫路由占位符) -->
    <!-- 将来通过路由规则匹配到的组件,将会被渲染到 router-view 所在的位置 -->
    <router-view></router-view>
    
  4. 定义路由组件

    const User = {
        template: '<div>User组件</div>'
    } 
    const Register = {
        template: '<div>Register组件</div>'
    } 
    
  5. 配置路由规则并创建路由实例

    // 创建路由实例对象
    var router = new VueRouter({
        // routes 是路由规则数组
        routes: [
            // 每个路由规则都是一个配置对象,其中至少包含 path 和 component 两个属性
            // path:表示当前路由规则匹配的 hash 地址
            // component:表示当前路由规则对应要展示的组件
            { path: '/User', component: User },
            { path: '/Register', component: Register }
        ]
    })
    
  6. 把路由挂载到 Vue 根实例中

    new Vue({
        el: '#app',
        // 为了能够让路由规则生效,必须把路由对象挂载到 vue 实例对象上
        // router: router // 在 ES6中变量名和变量值名称相同可以简写
        router
    })
    
8.2.2 路由重定向

路由重定向:用户在访问地址 A 时,强制用户跳转到地址 C,从而展示特定的组件页面;通过路由规则的 redirect 属性,指定一个新的路由地址,可以很方便地设置路由的重定向。

var router = new VueRouter({
    routes: [
        // path 表示需要被重定向的原地址;redirect 表示将要被重定向到的新地址
        { path: '/', redirect:'login' },
        { path: '/login', component: Login },
        { path: '/user', component: User },
        { path: '/register', component: Register }
    ]
})
8.2.3 嵌套路由用法
  1. 嵌套路由功能分析

    • 点击父级路由链接显示模板内容
    • 模板内容中又有子级路由链接
    • 点击子级路由链接显示自己模板内容

  2. 父路由组件模板

    • 父级路由链接
    • 父组件路由填充位
  3. 子级路由模板

    • 子级路由链接
    • 子级路由填充位
    const Register = {
       template: `
            <div>
                <div>Register 组件</div>
                <hr/>
                <!-- 子路由链接 -->
                <router-link to='/register/tab1'>Tab1</router-link>
                <router-link to='/register/tab2'>Tab2</router-link>
                <!-- 子路由占位符 -->
                <router-view></router-view>
            </div>
        `
    }
    
  4. 嵌套路由配置

    • 父级路由通过 children 属性配置子级路由
    const router = new VueRouter({
        routes: [
            { path: '/usr', component: User },
            {
                path: '/register',
                component: Register,
                // 通过 children 属性,为 /register 添加子路由规则
                children: [
                    { path: '/register/tab1', component: Tab1 },
                    { path: '/register/tab2', component: Tab2 }
                ]
            }
        ]
    })
    
8.2.4 动态路由匹配

应用场景:通过动态路由参数的模式进行路由匹配

const User  = {
    // 路由组件中通过 $route.params 获取路由参数
    template: '<div>User {{ $route.params.id }}</div>'
}
var router = new VueRouter({
    routes: [
        // 动态路径参数以冒号(:)开头
        { path: '/usr/:id', component: User }
    ]
})
8.2.5 路由组件传递参数

$route 与对应路由形成高度耦合,不够灵活,所以可以使用 props 将组件和路由解耦。

  1. props 的值为布尔类型

    const router = new VueRouter({
        routes: [
            // 如果 props 被设置为 true,route.params 将会被设置为组件属性
            { path: '/user/:id', component: User, props: true }
        ]
    })
    
    const User = {
        // 使用 props 接收路由参数
        props: ['id'],
        // 使用路由参数
        template: '<div>用户Id:{{ id }}</div>'
    }
    
  2. props 的值为对象类型

    const router = new VueRouter({
        routes: [
            // 如果 props 是一个对象,它会被按原样设置为组件属性            
            { 
                path: '/user/:id', 
                component: User, 
                props: { 
                    username: 'Tom', 
                    password: '123456'
                }
            }
        ]
    })
    
    const User = {
        // 使用 props 接收路由参数
        props: ['id', 'username', 'password'],
        // 使用路由参数
        // 这样是获取不到 id 值的,因为传递过来的 props 中没有 id
        template: '<div>用户信息:{{ id + '---' + username + '---' + password }}</div>'
    }
    
  3. props 的值为函数类型

    const router = new VueRouter({
        routes: [
            // 如果 props 是一个函数,则这个函数接收 route 对象为自己的形参
            {
                path: '/user/:id',
                component: User,
                props: route => ({ username: 'Tom', age: 20, id: route.params.id })
            }
        ]
    })
    
    const User = {
        props: ['username', 'age', 'id'],
        template: '<div>用户信息:{{ id + '---' + username + '---' + password }}</div>'
    }
    
8.2.6 命名路由的配置规则

为了更加方便的表示路由的路径,可以给路由规则起一个别名,即为“命令路由”。

const router = new VueRouter({
    routes: [
        {
            path: '/user/:id',
            // 命名路由
            name: 'user',
            component: User
        }
    ]
})
<!-- name 表示要跳转到的路由,params 表示路由要传递的参数 -->
<router-link :to="{ name: 'user', params: { id: 123  }}">User</router-link>

router.push({ name: 'user', params: { id: 123 }})
8.2.7 路由导航守卫控制访问权限

如果用户没有登录,但是直接通过 URL 访问特定页面,需要重新导航到登录页面。

// 为路由对象添加 beforeEach 导航守卫
router.beforeEach((to, from, next) => {
    // 如果用户访问的登录页,直接放行
    if (to.path === '/login') return next()
    
    // 从 sessionStorage 中获取到保存的 token 值
    const tokenStr = window.sessionStorage.getItem('token')
    // 没有 token,强制跳转到登录页
    if (!tokenStr) return next('/login')
    
    next()
})

8.3 编程式导航

8.3.1 页面导航的两种方式
  • 声明式导航:通过点击链接实现导航的方式,称为声明式导航
  • 编程式导航:通过调用 JavaScript 形式的 API 实现导航的方式,称为编程式导航。例如:普通网页的 location.href

常用的编程式导航 API 如下:

  • this.$router.push('hash地址')
  • this.$router.go(n)
const User = {
    template: `
        <div>
            <button @click="goRegister">跳转到注册页面</button>
            <button @click="goBack">后退</button>
        </div>
    `,
    methods: {
        goRegister: function() {
            // 用编程的方式控制路由跳转
            this.$router.push('/register');
        },
        goBack: function() {
            // 后退 
            this.$router.go(-1);
        }
    }
}
8.3.2 编程式导航参数规则

router.push() 方法的参数规则

// 字符串(路径名称)
router.push('/home')
// 对象
router.push({ path: '/home' })
// 命名的路由(传递参数)
router.push({ name: '/user', params: { id: 123 }})
// 带查询参数,变成 /register?username=Tom
router.push({ path: '/register', query: { username: 'Tom' }})

九、前端工程化

9.1 模块化相关规范

9.1.1 模块化概述

传统开发模式的主要问题:

  1. 命名冲突
  2. 文件依赖

通过模块化解决上述问题:

  • 模块化就是把单独的一个功能封装到一个模块(文件)中,模块之间相互隔离,但是可以通过特定的接口公开内部成员,也可以依赖别的模块。
  • 模块化开发的好处:方便代码的重用,从而提高开发效率,并且方便后期的维护
9.1.2 浏览器端模块化规范
  1. AMD

    Require.js(http://www.requirejs.cn/)

  2. CMD

    Sea.js(https://seajs.github.io/seajs/docs)

9.1.3 服务器端模块化规范
  1. CommonJS
    1. 模块分为单文件模块
    2. 模块成员导出:module.exports 和 exports
    3. 模块成员导入:require('模块标识符')
9.1.4 ES6 模块化

在 ES6 模块化规范之前,JavaScript 社区开始尝试并提出了 AMD、CDM、CommonJS 等模块化规范。

但是,这些社区提出的模块化标准,还是存在一定的差异性局限性、并不是浏览器与服务器通用的模块化标准,例如:

  • AMD 和 CMD 适用于浏览器端的 JavaScript 模块化
  • CommonJS 适用于服务器端的 JavaScript 模块化

因此,ES6 语法规范中,在语言从层面上定义了 ES6 模块化规范,是浏览器与服务器端通用的模块化开发规范。

ES6 模块化规范中定义:

  • 每个 js 文件都是一个独立的模块
  • 导入模块成员使用 import 关键字
  • 暴露模块成员使用 export 关键字
  1. Node.js 中通过 babel 体验 ES6 模块化

    • npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/node
    • npm install --save @babel/polyfill
    • 项目根目录创建 babel.config.js
    • babel.config.js 文件内容如下:
    const presets = [
        ["@babel/env", {
            targets: {
                edge: "17",
                firefox: "60",
                chrome: "67",
                safari: "11.1"
            }
        }]
    ];
    module.exports = { presets };
    
    • 通过 npx babel-node index.js 执行代码
9.1.5 ES6 模块化的基本语法
  1. 默认导出与默认导入

    • 默认导出语法:export default 默认导出的成员

      // 定义私有成员
      let a = 10;
      let c = 20;
      let d = 30;
      function show() {}
      
      // 将本模块的私有成员暴露出去,供其它模块使用
      export default {
          a,
          c,
          show
      }
      
    • 默认导入语法:import 接收名称 from '模块标识符'

      // 导入模块成员
      import m1 from './m1.js'
      
      console.log(m1);
      // 打印输出的结果:{ a: 10, c: 20, show: [Function: show] }
      

    注意:每个模块中,只允许使用唯一的一次 export default,否则会报错~

  2. 按需导出与按需导入

    • 按需导出语法:export let s1 = 10

      // 向外按需导出变量 s1、s2
      export let s1 = 'aaa';
      export let s2 = 'ccc';
      // 向外按需导出方法 say
      export function say = function() {}
      
    • 按需导入语法:import { s1 } from '模块标识符'

      // 导入模块成员
      import { s1, s2 as ss2, say } from './m1.js'
      
      console.log(s1);
      console.log(s2);
      console.log(say);  // [Function: show]
      

      注意:每个模块中,可以使用多次按需导出。

  3. 直接导入并执行模块代码

    只想单纯执行某个模块中的代码,并不需要得到模块中向外暴露的成员,此时,可以直接导入并执行模块代码。

    // m2.js
    // 在当前模块中执行一个 for 循环操作
    for(let i = 0; i < 10; i++) {
        console.log(i);
    }
    
    // 直接导入并执行模块代码
    import './m2.js'
    

9.2 webpack

9.2.1 web 开发面临的困境
  • 文件依赖关系错综复杂
  • 静态资源请求效率低
  • 模块化支持不友好
  • 浏览器对高级 JavaScript 特性兼容程度较低
  • ...
9.2.2 webpack 概述

webpack 是一个流行的前端项目构建工具(打包工具),可以解决当前 web 开发中所面临的困境。

webpack 提供了友好的模块化支持,以及代码压缩混淆处理 js 兼容问题性能优化等强大的功能,从而让程序员把工作的重心放到具体的功能实现上,提高了开发效率和项目的可维护性。

目前绝大多数企业中的前端项目,都是基于 webpack 进行打包构建的。

官网:https://webpack.js.org/

9.2.3 webpack 的基本使用
  1. 项目初始化的一些操作

    • 新建项目空白目录,运行 npm init -y 命令,初始化包管理配置文件 package.json
    • 新建 src 源代码目录
    • 新建 src -> index.html 首页
    • 初始化首页基本的结构
    • 运行 npm install jquery -S 命令,安装 jQuery。
  2. 在项目中安装和配置 webpack

    • 运行 npm install webpack webpack-cli -D 命令,安装 webpack 相关的包

    • 在项目根目录中,创建名为 webpack.config.js 的 webpack 配置文件

    • 在 webpack 的配置文件中,初始化如下基本配 置:

      module.exports = {
          mode: 'development'   // mode 指定构建模式
      }
      
    • 在 package.json 配置文件中的 scripts 节点中,新增 dev 脚本如下:

      "scripts": {
          "dev": "webpack"  // script 节点下的脚本,可以通过 npm run 运行
      }
      
    • 在终端运行 npm run dev 命令,启动 webpack 进行项目打包。

  3. 配置打包的入口和出口

    webpack 的 4.x 版本中默认约定:

    • 打包的入口文件为 src -> index.js
    • 打包的输出文件为 dis -> main.js

    如果要修改打包的入口和出口,可以在 webpack.config.js 中新增如下配置信息:

    // 导入 node.js 中专门操作路径的模块
    const path = require('path');
    module.exports = {
        // 打包入口文件的路径
        entry: path.join(__dirname, './src/index.js');
        output: {
            // 输出文件的存放路径
            path: path.join(__dirname, './dist'),
            // 输出文件的名称
            filename: 'bundle.js'
        }
    }
    
  4. 配置 webpack 的自动打包功能

    • 运行 npm install webpack-dev-server -D 命令,安装支持项目自动打包的工具

    • 修改 package.json -> scripts 中的 dev 命令如下:

      "scripts": {
          "dev": "webpack-dev-server" // script 节点下的脚本,可以通过 npm run 运行 
      }
      
    • 将 src -> index.html 中,script 脚本的引用路径,修改为 "/bundle.js"

    • 运行 npm run dev 命令,重新进行打包

    • 在浏览器中访问 http://localhost:8080 地址,查看自动打包效果

    注意:webpack、webpack-cli、webpack-server 的版本兼容问题。下面是可用的版本信息:

    "devDependencies": {
        "webpack": "^4.43.0",
        "webpack-cli": "^3.3.11",
        "webpack-dev-server": "^3.10.3"
    }
    

    注意:webpack-dev-server 会启动一个实时打包的 http 服务器,引入的 /bundle.js 并不在工程目录中,而是在内存中。

  5. 配置 html-webpack-plugin 生成预览页面

    • 运行 npm install html-webpack-plugin -D 命令,安装生成预览页面的插件

    • 修改 webpack.config.js 文件头部区域,添加如下配置信息:

      // 导入生成预览页面的插件,得到一个构造函数
      const HtmlWebpackPlugin = require('html-webpack-plugin');
      // 创建插件的实例对象
      const htmlPlugin = new HtmlWebpackPlugin({
          // 指定要用到的模板文件
          template: './src/index.html',
          // 指定生成的文件名称,该文件存在于内存中,在目录中不显示
          filename: 'index.html'
      })
      
    • 修改 webpack.config.js 文件中向外暴露的配置对象,新增如下配置节点:

      module.exports = {
          plugins: [ htmlPlugin ]  // plugins 数组是 webpack 打包期间会用到的一些插件列表
      }
      
  6. 配置自动打包相关的参数

    // package.json 中的配置
    // --open 打包完成后自动打开浏览器页面
    // --host 配置 IP 地址
    // --port 配置端口
    "scripts": {
        "dev": "webpack-dev-serveer --open --host 127.0.0.1 --port 8888"
    }
    
9.2.4 webpack 中的加载器
  1. 通过 loader 打包非 js 模块

    在实际开发过程中,webpack 默认只能打包处理以 .js 后缀名结尾的模块,其他非 .js 后缀名结尾的模块,webpack 默认处理不了,需要调用 loader 加载器才可以正常打包,否则会报错。

    loader 加载器可以协助 webpack 打包处理特定的文件模块,比如:

    • less-loader 可以打包处理 .less 相关的文件
    • sass-loader 可以打包处理 .scss 相关的文件
    • url-loader 可以打包处理 css 中与 url 路径相关的文件
  2. loader 的调用过程

9.2.5 webpack 中加载器的基本使用
  1. 打包处理 css 文件

    • 运行 npm i style-loader css-loader -D 命令,安装处理 css 文件的 loader

    • 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:

      // 所有第三方文件模块的匹配规则
      module: {
          rules: [
              {
                  // 匹配的文件类型
                  test: /.css$/, 
                  // 对应要调用的 loader
                  use: ['style-loader', 'css-loader'] 
              }
          ]
      }
      

      注意:

      • use 数组中指定的 loader 顺序是固定的
      • 多个 loader 的调用顺序是:从后往前调用。先将 css 文件交给 css-loader 处理,再将处理后的结果提交给 style-loader 处理。
  2. 打包处理 less 文件

    • 运行 npm i less-loader less -D 命令

    • 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:

      // 所有第三方文件模块的匹配规则
      module: {
          rules: [
              {
                  test: /.less$/,
                  use: ['style-loader', 'css-loader', 'less-loader']
              }
          ]
      }
      
  3. 打包处理 scss 文件

    • 运行 npm i sass-loader node-sass -D 命令

    • 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:

      module: {
          rules: [
              {
                  test: /.scss$/,
                  use: ['style-loader', 'css-loader', 'sass-loader']
              }
          ]
      }
      
  4. 配置 postCSS 自动添加 css 的兼容前缀

    • 运行 npm i postcss-loader autoprefixer -D 命令

    • 在项目根目录中创建 postcss 的配置文件 postcss.config.js,并初始化如下配置:

      // 导入自动添加前缀的插件
      const autoprefixer = require('autoprefixer')
      module.exports = {
          //  挂载插件
          plugins: [ autoprefixer ] 
      }
      
    • 在 webpack.config.js 的 module -> rules 数组中,修改 css 的 loader 规则如下:

      module: {
          rules: [
              {
                  test: /.css$/,
                  use: ['style-loader', 'css-loader', 'postcss-loader']
              }
          ]
      }
      
  5. 打包样式表中的图片和字体文件

    • 运行 npm i url-loader file-loader -D 命令

    • 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:

      module: {
          rules: [
              {
                  test: /.jpg|png|gif|bmp|ttf|eot|svg|woff|woff2$/,
                  use: 'url-loader?limit=16940'
              }
          ]
      }
      

      其中 ? 之后的是 loader 的参数项。

      limit 用来指定图片的大小,单位是字节(byte),只有小于 limit 大小的图片,才会被转为 base64 图片。

  6. 打包处理 js 文件中的高级语法

    • 安装 babel 转换器相关的包 npm i babel-loader @babel/core @babel/runtime -D

    • 安装 babel 语法插件相关的包:npm i @babel/preset-env @babel/plugin-transform-runtime @babel/plugin-proposal-class-properties -D

    • 在项目根目录中,创建 babel 配置文件 babel.config.js 并初始化基本配置如下:

      module.exports = {
          presets: [ '@babel/preset-env' ],
          plugins: [ '@babel/plugin-transform-runtime', '@babel/plugin-proposal-class-properties' ]
      }
      
    • 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:

      // exclude 为排除项,表示 babel-loader 不需要处理 node_modules 中的 js 文件
      {
          test: /.js$/,
          use: 'babel-loader',
          exclude: /node_modules/ 
      }
      

9.3 Vue 单文件组件

9.3.1 传统组件的问题和解决方案
  1. 问题

    • 全局定义的组件必须保证组件的名称不重复
    • 字符串模板缺乏语法高亮,在 HTML 有多行的时候,需要用到丑陋的反斜杠
    • 不支持 CSS 意味着当 HTML 和 JavaScript 组件化时,CSS 明显被遗漏
    • 没有构建步骤限制,只能使用 HTML 和 ES5 JavaScript,而不能使用预处理器(如:babel)
  2. 解决方案

    针对传统组件的问题,Vue 提供了一个解决方案:使用 Vue 单文件组件。

9.3.2 Vue 单文件组件的基本用法

单文件组件的组成结构

  • template 组件的模板区域
  • script 业务逻辑区域
  • style 样式区域
<template>
    <!-- 这里用于定义 Vue 组件的模板内容 -->
</template>

<script>
    // 这里用于定义 Vue 组件的业务逻辑
    export default {
        data: () { return {} },  // 私有数据
        methods: {}  // 处理函数
        // ... 其它业务逻辑
    }
</script>

<style scoped>
    /* 这里用于定义组件的样式 */
</style>
9.3.3 webpack 中配置 vue 组件的加载器
  1. 运行 npm i vue-loader vue-template-compiler -D 命令

  2. 在 webpack.config.js 配置文件中,添加 vue-loader 的配置项如下:

    const VueLoaderPlugin = require('vue-loader/lib/plugin')
    module.exports = {
        module: {
            // ... 其它规则
            rules: [
                {
                    test: /.vue$/,
                    loader: 'vue-loader'
                }
            ]
        },
        plugins: [
            // ... 其它插件
            new VueLoaderPlugin()  // 请确保引入这个插件
        ]
    }
    
9.3.4 在 webpack 项目中使用 vue
  1. 运行 npm i vue -s 安装 vue;
  2. 在 src -> index.js 入口文件中,通过 import Vue from 'vue' 来导入 vue 构造函数;
  3. 通过 vue 的实例对象,并指定要控制的 el 区域;
  4. 通过 render 函数渲染 App 根组件
// 1. 导入 Vue 构造函数
import Vue from 'vue'
// 2. 导入 App 根组件
import App from './components/App.vue'

const vm = new Vue({
    // 3. 指定 vm 实例要控制的页面区域
    el: '#app',
    // 4. 通过 render 函数,把指定的组件渲染到 el 区域中
    render: h => h(App)
})
9.3.5 webpack 打包发布

上线之前需要通过 webpack 将应用进行整体打包,可以通过 package.json 文件配置打包命令:

// 在 package.json 文件中配置 webpack 打包命令
// 该命令默认加载项目根目录中的 webpack.config.js 配置文件
"scripts": {
    // 用于打包的命令
    "build":  "webpack -p",
    // 用于开发调试的命令
    "dev": "webpack-dev-server --open --host 127.0.0.1 --port 3000",
},

9.4 Vue 脚手架

9.4.1 Vue 脚手架的基本用法

Vue 脚手架用于快速生成 Vue 项目基础框架。官网地址:https://cli.vuejs.org/zh/

使用步骤:

  1. 安装 Vue 脚手架

    • npm install -g @vue/cli
  2. 脚手架创建 vue 项目

    // 1. 基于交互式命令行的方式,创建新版 vue 项目
    vue create my-project
    
    // 2. 基于图形化界面的方式,创建新版 vue 项目
    vue ui
    
    // 3. 基于 2.x 的旧模板,创建旧版 vue 项目
    npm install -g @vue/cli-init
    vue init webpack my-project 
    
9.4.2 Vue 脚手架生成的项目结构分析

9.4.3 Vue 脚手架的自定义配置
  1. 通过 package.json 配置项目

    // 必须是符合规范的 json 语法
    "vue": {
        "devServer": {
            "port": "8888",
            "open": true
        }
    }
    

    注意:不推荐使用这种配置方式。因为 package.json 主要用来管理包的配置信息;为了方便维护,推荐将 vue 脚手架相关的配置,单独定义到 vue.config.js 配置文件中。

  2. 通过单独的配置文件配置项目

    • 在项目的根目录中创建文件 vue.config.js
    • 在该文件中进行相关配置,从而覆盖默认配置
    // vue.config.js
    module.exports = {
        devServer: {
            port: 8888
        }
    }
    

9.5 Element-UI 的基本使用

Element-UI:一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库

官网地址:http://element-cn.eleme.io/#/zh-CN

  1. 基于命令行方式手动安装

    • 安装依赖包 npm i element-ui -S
    • 导入 Element-UI 相关资源
    // 导入组件库
    import ElementUI from 'element-ui';
    // 导入组件相关样式
    import 'element-ui/lib/theme-chalk/index.css';
    // 配置 Vue 插件
    Vue.use(ElementUI);
    
  2. 基于图形化界面自动安装

    • 运行 vue ui 命令,打开图形化界面
    • 通过 Vue 项目管理器,进入具体的项目配置面板
    • 点击 插件 -> 添加插件,进入插件查询面板
    • 搜索 vue-cli-plugin-element 并安装
    • 配置插件,实现按需导入,从而减少打包后项目的体积

十、项目优化

10.1 项目优化策略

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

10.2 生成打包报告

打包时,为了直观地发现项目中存在的问题,可以在打包时生成报告。

生成报告的方式有两种:

  1. 通过命令行参数的形式生成报告

    // 通过 vue-cli 的命令选项可以生成打包报告
    // --report 选项可以生成 report.html 以帮助分析包内容
    vue-cli-service build --report
    
  2. 通过可视化的 UI 面板直接查看报告

    在可视化的 UI 面板中,通过控制台分析面板,可以方便的看到项目中存在的问题。


10.3 通过 vue.config.js 修改 webpack 的默认配置

通过 vue-cli 3.0 工具生成的项目,默认隐藏了所有 webpack 的配置项,目的是为了屏蔽项目的配置过程,让程序员把工作的重心,放到具体功能和业务逻辑的实现上。

如果程序员有修改 webpack 默认配置的需求,可以在项目根目录中,按需创建 vue.config.js 这个配置文件,从而对项目的打包发布过程做自定义的配置(具体配置参考 https://cli.vuejs.org/zh/config/#vue-config-js

// vue.config.js
// 导出一个包含了自定义配置选项的对象
module.exports = {

}
  1. 为开发模式和发布模式指定不同的打包入口

    默认情况下,Vue 项目的开发模式和发布模式,共用同一个打包的入口文件(即 src/main.js)。为了将项目的开发过程和发布过程分离,我们可以为两种模式,各自指定打包的入口文件,即:

    • 开发模式的入口文件为 src/main-dev.js
    • 发布模式的入口文件为 src/main-prd.js
  2. configureWebpack 和 chainWebpack

    在 vue.config.js 导出的配置对象中,新增 configureWebpack chainWebpack 节点,来自定义 webpack 的打包配置。

    configureWebpack 和 chainWebpack 的作用相同,唯一的区别就是它们修改 webpack 配置的方式不同:

    • chainWebapck 通过链式编程的形式,来修改默认的 webpack 配置
    • configureWebpack 通过操作对象的形式,来修改默认的 webpack 配置

    两者具体的使用差异,可参考如下网址:https://cli.vuejs.org/zh/guide/webpack.html#webpack-相关

  3. 通过 chainWebpack 自定义打包入口

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

10.4 通过 externals 加载外部 CDN 资源

默认情况下,通过 import 语法导入的第三方依赖包,最终会被打包合并到同一个文件中,从而导致打包成功后,单文件体积过大的问题。

为了解决上述问题,可以通过 webpack 的 externals 节点,来配置并加载外部的 CDN 资源。凡是声明在 externals 中的第三方依赖包,都不会被打包。

config.set('externals', {
    vue: 'vue',
    'vue-router': 'VueRouter',
    axios: 'axios',
    lodash: '_',
    echarts: 'echarts',
    nprogress: 'NProgress',
    'vue-quill-editor': 'VueQuillEditor'
})

同时,需要在 public/index.html 文件的头部,添加如下的 CDN 资源引用:

<!-- nprogress 的样式表文件 -->
<link rel="stylesheet" href="https://cdn.staticfile.org/nprogress/0.2.0/nprogress.min.css" />
<!-- 富文本编辑器的样式文件 -->
<link rel="stylesheet" href="https://cdn.staticfile.org/quill/1.3.4/quill.core.min.css" />
<link rel="stylesheet" href="https://cdn.staticfile.org/quill/1.3.4/quill.snow.min.css" />
<link rel="stylesheet" href="https://cdn.staticfile.org/quill/1.3.4/quill.bubble.min.css" />

<script src="https://cdn.staticfile.org/vue/2.5.22/vue.min.js"></script>
<script src="https://cdn.staticfile.org/vue-router/3.0.1/vue-router.min.js"></script>
<script src="https://cdn.staticfile.org/axios/0.18.0/axios.min.js"></script>
<script src="https://cdn.staticfile.org/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://cdn.staticfile.org/echarts/4.1.0/echarts.min.js"></script>
<script src="https://cdn.staticfile.org/nprogress/0.2.0/nprogress.min.js"></script>
<!-- 富文本编辑器的 js 文件 -->
<script src="https://cdn.staticfile.org/quill/1.3.4/quill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-quill-editor@3.0.4/dist/vue-quill-editor.js"></script>

10.5 通过 CDN 优化 ElementUI 的打包

虽然在开发阶段,启用了 element-ui 组件的按需加载,尽可能的减少了打包的体积,但是那些被按需加载的组件,还是占用了较大的文件体积。此时,我们可以将 element-ui 中的组件,也通过 CDN 的形式来加载,这样能够进一步减小打包后的文件体积。

具体操作流程:

  1. 在 main-prd.js 中,注释掉 element-ui 按需加载的代码
  2. 在 index.html 的头部区域中,通过 CDN 加载 element-ui 的 js 和 css 样式
<!-- 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>

10.6 首页内容自定制

不同的打包环境下,首页内容可能会有所不同。我们可以通过插件的方式进行定制,插件配置如下:

chainWebpack: config => {
    config.when(process.env.NODE_ENV === 'production', config => {
        config.plugin('html').tap(args => {
            args[0].isPrd = true
            return args
        })
    })
    config.when(process.env.NODE_ENV === 'development', config => {
        config.plugin('html').tap(args => {
            args[0].isPrd = false
            return args
        })
    })
}

public/index.html 首页中,可以根据 isPrd 的值来决定如何渲染页面结构:

<!-- 按需渲染页面的标题 -->
<title><%= htmlWebpackPlugin.options.isPrd ? '' : 'dev - ' %>电商后台管理系统</title>

<!-- 按需加载外部的 CDN 资源 -->
<% if (htmlWebpackPlugin.options.isPrd) { %>
    <!-- 通过 externals 加载的外部 CDN 资源 -->
<% } %>

10.7 路由懒加载

当打包构建项目时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

使用步骤:

  1. 安装 @bebel/plugin-syntax-dynamic-import

  2. babel.config.js 配置文件中声明该插件

    module.exports = {
      presets: [
        '@vue/cli-plugin-babel/preset'
      ],
      plugins: [
        ...
        '@babel/plugin-syntax-dynamic-import'
      ]
    }
    
  3. 将路由改为按需加载的形式,实例代码如下:

    const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
    const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
    const Baz = () => import(/* webpackChunkName: "group-boo" */ './Baz.vue')
    

解释:

  • './Foo.vue' 是路由组件存放的位置
  • /* webpackChunkName: "group-foo" */ 是路由的分组

在上面的实例代码中,Foo 和 Bar 被打包进了同一个 js 文件,在请求 Foo 组件时,也会把 Bar 组件请求到。

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


十一、项目上线

11.1 项目上线相关配置

  1. 通过 node 创建 web 服务器
  2. 开启 gzip 配置
  3. 配置 https 服务
  4. 使用 pm2 管理应用

11.2 通过 node 创建 web 服务器

创建 node 项目,并安装 express,通过 express 快速创建 web 服务器,将 vue 打包生成的 dist 文件夹,托管为静态资源即可,关键代码如下:

const express = require('express')
// 创建 web 服务器
const app = express()

// 托管静态资源
app.use(express.static('./dist'))

// 启动 web 服务器
app.listen(3000, () => {
    console.log('web server running at http://127.0.0.1:3000')
})

11.3 开启 gzip 配置

使用 gzip 可以减小文件体积,是传输速度更快。

可以通过服务器使用 express 做 gzip 压缩,配置如下:

// 安装包
npm install compression -D

// 导入包
const compression = require('compression')
// 启用中间件
app.use(compression)

11.4 配置 HTTPS 服务

  • 传统的 HTTP 协议传输的数据都是明文,不安全
  • 采用 HTTPS 协议对传输的数据进行了加密处理,可以防止数据被中间人窃取,使用更安全。

申请 SSL 证书(https://freessl.org

  1. 进入 https://freessl.cn/ 官网,输入要申请的域名并选择品牌
  2. 输入自己的邮箱并选择相关选项
  3. 验证 DNS(在域名管理后台添加 TXT 记录)
  4. 验证通过之后,下载 SSL 证书(full_chain.pem 公钥;private.key 私钥)

在后台项目中导入证书

const https = require('https')
const fs = require('fs')
const options = {
    cert: fs.readFileSync('./full_chain.pem'),
    key: fs.readFileSync('./private.key')
}
https.createServer(options, app).listen(443)

11.5 使用 pm2 管理应用

  1. 在服务器中安装 pm2:npm i pm2 -g
  2. 启动项目:pm2 start 脚本 --name 自定义名称pm2 start app.js --name web_vueshop
  3. 查看运行项目:pm2 ls
  4. 重启项目:pm2 restart 自定义名称
  5. 停止项目:pm2 stop 自定义名称
  6. 删除项目:pm2 delete 自定义名称

十二、Vuex

12.1 Vuex 概述

  1. Vue组件间共享数据的方式

    • 父向子传值:v-bind 属性绑定
    • 子向父传值:v-on 事件绑定
    • 兄弟组件之间共享数据:EventBus
      • 接收数据的那个组件使用 $on
      • 发送数据的那个组件使用 $emit
  2. Vuex 是什么

    Vuex 是实现组件全局状态(数据)管理的一种机制,可以方便的实现组件之间数据的共享。

  3. 使用 Vuex 统一管理状态的好处

    • 能够在 vuex 中集中管理共享的数据,易于开发和后期维护;
    • 能够高效地实现组件之间的数据共享,提高开发效率
    • 存储在 vuex 中的数据都是响应式的,能够实时保持数据与页面的同步
  4. 什么样的数据适合存储在 Vuex 中

    一般情况下,只有组件之间共享的数据,才有必要存储在 vuex 中;对于组件中的私有数据,依旧存储在组件自身的 data 中即可。

12.2 Vuex 的基本使用

  1. 安装 vuex 依赖包

    npm install vuex --save
    
  2. 导入 vuex 包

    import Vuex from 'vuex'
    Vue.use(Vuex)
    
  3. 创建 store 对象

    const store = new Vuex.Store({
        // state 中存放的就是全局共享的数据
        state:  { count: 0 }
    })
    
  4. 将 store 对象挂载到 vue 实例中

    new Vue({
        el: '#app',
        render: h => h(app),
        router,
        // 将创建的共享数据对象挂载到 Vue 实例中
        // 所有的组件,就可以直接从 store 中获取全局的数据了
        store
    })
    

12.3 Vuex 的核心概念

Vuex 中的主要核心概念:

  • State
  • Mutation
  • Action
  • Getter
12.3.1 State

State 提供唯一的公共数据源,所有共享的数据都要统一放到 Store 的 State 中进行存储。

// 创建 store 数据源,提供唯一公共数据
const store = new Vuex.Store({
    state: { count: 0 }
})

组件访问 State 中数据的第一种方式:

// this.$store.state.全局数据名称
this.$store.state.count

组件访问 State 中数据的第二种方式:

// 1. 从 vuex 中按需导入 mapState 函数
import { mapState } from 'vuex'

通过刚才导入的 mapState 函数,将当前组件需要的全局数据,映射为当前组件的 computed 计算属性:

// 2. 将全局数据映射为当前组件的计算属性
computed: {
    ...mapState(['count'])
}
12.3.2 Mutation

Mutation 用于变更 Store 中的数据。

  1. 只能通过 mutation 变更 Store 数据,不可以直接操作 Store 中的数据
  2. 通过这种方式虽然操作起来稍微繁琐一些,但是可以集中监控所有数据的变化。
// 定义 Mutation
const store = new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        add(state) {
            // 变更状态
            state.count++
        }
    }
})

组件中触发 mutation

methods: {
    btnClicked() {
        // 触发 mutations 的第一种方式
        this.$store.commit('add')
    }
}

可以在触发 mutations 时传递参数:

// 定义 mutations
const store = new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        addN(state, step) {
            // 变更状态 
            state.count += step
        }
    }
})

组件中触发 mutation

methods: {
    btnClicked() {
        // 在调用 commit 函数触发 mutations 时携带参数
        this.$store.commit('addN', 3)
    }
}

this.$store.commit() 是触发 mutations 的第一种方式,触发 mutation 的第二种方式:

// 1. 从 vuex 中按需导入 mapMutations 函数
import { mapMutations } from 'vuex'

通过刚才导入的 mapMutations 函数,将需要的 mutations 函数映射为当前组件的 methods 方法:

// 2. 将指定的 mutations 函数映射为当前组件的 methods 函数
methods: {
    ...mapMutations(['add', 'addN'])
}

不要在 mutations 函数中执行异步操作

12.3.3 Action

Action 用于处理异步任务。

如果通过异步操作变更数据,必须通过 Action,而不能使用 Mutation,但是在 Action 中还是要通过触发 Mutation 的方法间接变更数据。

// 定义 Action
const store = new Vuex.Store({
    ...
    mutatios: {
        add(state) {
            state.count++
        }
    },
    actions: {
        addAsync(context) {
            setTimeout(() => {
                context.commit('add')
            }, 1000)
        }
    }
})

组件中触发 Action:

methods: {
    btnClicked() {
        // 触发 actions 的第一种方式
        this.$store.dispatch('addAsync')
    }
}

触发 actions 异步任务时携带参数:

const store = new Vuex.store({
    ...
    mutations: {
        addN(state, step) {
            state.count += step
        }
    },
    actions: {
        addNAsync(context, step) {
            setTimeout(() => {
                context.commit('addN', step)
            }, 1000)
        }
    }
})

组件中触发 Action:

methods: {
    btnClicked() {
        this.$store.dispatch('addNAsync', 2)
    }
}

触发 actions 的第二种方式:

// 1. 从 vuex 中按需导入 mapActions 函数
import { mapActions } from 'vuex'

通过刚导入的 mapActions 函数,将需要的 actions 函数映射为当前组件的 methods 方法:

// 2. 将指定的 actions 函数映射为当前组件的 methods 函数
methods: {
    ...mapActions(['addAsync', 'addNAsync'])
}
12.3.4 Getter

Getter 用于对 Store 中的数据进行加工处理形成新的数据。

  1. Getter 可以对 Store 中已有的数据加工处理之后形成新的数据,类似 Vue 的计算属性。不会修改原数据。
  2. Store 中数据发生变化,Getter 的数据也会跟着变化。
const store = new Vuex.Store({
    state: {
        count: 0
    },
    getters: {
        showNum: state => {
            return '当前最新的数量是【' + state.count + '】'
        }
    }
})

使用 getters 的第一种方式:

// this.$store.getters.名称
this.$store.getters.showNum

使用 getters 的第二种方式:

import { mapGetters } from 'vuex'

computed: {
    ...mapGetters(['showNum'])
}
原文地址:https://www.cnblogs.com/dins/p/13936910.html