vue.js学习

401 阅读43分钟

初识Vuejs

Vue是一个渐进式的框架,什么是渐进式?

  • 可以作为应用的一部分将vue.js引入项目
  • 如果更多的业务逻辑使用Vue实现,那么Vue的核心库以及生态系统(Core + Vue-router + Vuex),可以满足各种需求

Vue的一些特点和高级功能

  • 数据驱动视图
  • 虚拟DOM
  • 可复用组件
  • 前端路由
  • 状态管理

Vue.js安装

使用一个框架,第一步是安装它,安装方式主要有一下几种方式:

  • 方式1:直接CDN引入(缺点:使用过程要连网,资源是从网上)

    <!-- 开发环境版本,包含了有帮助的命令行警告 --> 
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <!-- 生产环境版本,优化了尺寸和速度 -->
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
    
  • 方式2:下载并引用

    <!--开发环境-->
    https://vuejs.org/js/vue.js
    <!--生产环境-->
    https://vuejs.org/js/vue.min.js
    
  • 方式3:NPM安装
    通过webpack和CLI方式开发项目,使用这种方式

Vue的MVVM

MVVM

  • View层
    视图层,在前端开发中,通常指的是DOM层,用于信息(数据)的展示
  • Model层
    数据层
    可能是固定的死数据,也可能是通过网络请求下来的服务器上的数据
  • ViewModel层
    视图模型层
    视图模型层是view和model沟通的桥梁,一方面它实现了数据绑定(Data Binding),将Model的改变实时的反应到view中;另一方面实现了DOM监听(DOM Listener),当DOM发生一些事件(点击、滚动)时,可以监听到,在需要情况下通过methods等手段改变对应的data

Vue的生命周期

Vue的生命周


vue语法

主要包括

  • 插值操作
  • 绑定属性
  • 计算属性
  • vue过滤器
  • 事件监听
  • 条件判断
  • 循环遍历
  • v-model

插值操作(Mustache)

如何将data中的数据,插入到HTML元素中?
通过Mustache语法(双大括号)

通过mustache语法插入的数据,数据是响应式的

<body>
    <div id="app">
        <h3>hello {{message}}</h3>
        <h3>{{firstName}}{{lastName}}</h3>
        <!--可以是表达式-->
        <h3>{{ couter * 2 }}</h3>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        data: {
            message: "VueJs!",
            firstName: '中国',
            lastName: '功夫',
            couter: 100
        }
    })
</script>

v-once
在某些情况下,我们不希望界面随数据的变化而改变,就可以使用v-once指令

该指令后面不需要跟任何表达式
使用该指令的元素和组件只渲染一次,不会随着数据的改变而改变

<body>
    <div id="app">
        <h3 v-once>hello {{message}}</h3>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        data: {
            message: "VueJs!",
        }
    })
</script>

v-html
某些情况下,从后台服务器请求到的数据本身就是一个HTML代码
如果直接通过{{}}输出,会将HTML代码输出

如果我们希望输出的内容,按照HTML的格式解析渲染,就需要使用 v-html 指令

<body>
    <div id="app">
        <div v-html='dataHtml'></div>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        data: {
            dataHtml: '<ul><li>西游记</li><li>水浒传</li></ul>',
        }
    })
</script>

v-text
v-text的作用和Mustache类似:将数据显示在界面上
但是使用起来没有 Mustache语法 灵活

<body>
    <div id="app">
        <div v-text='message'>hello</div>
        <div>hello {{ message}}</div>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        data: {
            message: 'VueJS'
        }
    })
</script>

v-pre
v-pre 用于跳过这个元素和子元素的编译过程,用于显示原本的Mustache 语法

<body>
    <div id="app">
        <div>hello {{ message}}</div>
        <div v-pre>hello {{ message}}</div>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        data: {
            message: 'VueJS'
        }
    })
</script>

v-cloak
在某些情况下,浏览器由于渲染太慢或网速太慢会显示未编译的Mustache标签
使用v-clock配合样式,可以在没渲染完成之前隐藏这个元素,渲染后再显示

<style>
    [v-cloak] {
        display: none;
    }
</style>

<body>
    <div id="app">
        <div v-cloak>hello {{ message}}</div>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        data: {
            message: 'VueJS'
        }
    })
</script>

属性绑定(v-bind)

Mustache({{ }}),主要的作用是将数据插入到模板中
但是,在实际开发中,元素的属性也是需要动态绑定

  • 比如,a元素的href属性
  • img的src属性
  • 元素的class和style 动态绑定

这些功能的实现就需要使用 v-bind

v-bind 用于绑定一个或多个属性值,或者向子组件传递props值

下面,通过data中的数据动态绑定src和href

<body>
    <div id="app">
        <a v-bind:href="vueLink">VueJs官网</a>
        
        <!--v-bind语法糖写法-->
        <a :href="vueLink">VueJs官网</a>
        <img :src="logoSrc" alt="vue官网图片">
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        data: {
            vueLink: 'https://cn.vuejs.org/',
            logoSrc: 'https://cn.vuejs.org/images/logo.png'
        }
    })
</script>

v-bind绑定class
绑定class有两种方式

  • 对象语法
  • 数组语法

绑定方式:对象语法

<style>
    .active {
        color: red;
    }
    
    .fontSize {
        font-size: 28px;
    }
    
    .backGround {
        background-color: blue;
    }
</style>

<body>
    <div id="app">
        <!-- 可以传入多个值 -->
        <div :class="{active: isAcive, fontSize: isFontSize }">hello world!</div>

        <!-- 和普通类同时存在,不冲突 -->
        <div :class="{active: isAcive, fontSize: isFontSize }" class="backGround">hello world!</div>

        <!-- 如果过于复杂,可以放在一个methods 或computed -->
        <div :class="classes" class="backGround">hello world!</div>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        data: {
            isAcive: true,
            isFontSize: true,
        },
        computed: {
            classes() {
                return {
                    active: this.isAcive,
                    fontSize: this.isFontSize
                }
            }
        },
    })
</script>

绑定方式:数组语法 (不常用,了解即可)

<style>
    .active {
        color: red;
    }
    
    .fontSize {
        font-size: 28px;
    }
    
    .backGround {
        background-color: blue;
    }
</style>

<body>
    <div id="app">
        <!-- 可以传入多个值 -->
        <div :class="['active', 'fontSize']">hello world!</div>

        <!-- 和普通类同时存在,不冲突 -->
        <div :class="['active', 'fontSize']" class="backGround">hello world!</div>

        <!-- 如果过于复杂,可以放在一个methods 或computed -->
        <div :class="classes" class="backGround">hello world!</div>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        computed: {
            classes() {
                return ['active', 'fontSize']
            }
        },
    })
</script>

v-bind绑定style
利用v-bind:style 可以绑定一些css内联样式

注意在写CSS属性名时,比如:font-size

  • 我们可以使用驼峰式 fontSize
  • 或短横钱分割(kbab-case, 记得用单引号括起来) 'font-size'

绑定class有两种方式:

  • 对象语法
  • 数组语法

绑定方式1:对象语法
style后面跟的是一个对象类型
  对象的key是css属性名称
  对象的value是具体的值,可能来自data中属性,也可能父组件传递的值

<body>
    <div id="app">

        <div :style="{color: currentColor, fontSize: fontSize + 'px', 'background-color':backgroundColor }">hello world!</div>

    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        data: {
            currentColor: 'red',
            fontSize: 24,
            backgroundColor: 'blue'
        },
    })
</script>

绑定方式2:数组语法(不常用)

<body>
    <div id="app">

        <div :style="[colorStyle, fontSizeStyle]">hello world!</div>

    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        data: {
            colorStyle: {
                color: 'red'
            },
            fontSizeStyle: {
                fontSize: '28px'
            }
        },
    })
</script>

计算属性

在模板中可以直接通过插值语法显示一些data中的数据
但是某些情况下,需要对数据进行一些转化后显示,或者多个数据结合后显示

下面,一个名字由firstName 和 lastName两个变量组合而成

<body>
    <div id="app">
        <!-- 单纯使用插值语法 -->
        <div>全名:{{ firstName }}{{ lastName }}</div>

        <!-- 使用计算属性 -->
        <div>全名:{{ fullName }}</div>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        data: {
            firstName: 'hello',
            lastName: 'VueJs'
        },
        computed: {
            fullName() {
                return this.firstName + this.lastName;
            }
        },
    })
</script>

也可以计算比较复杂的操作,如下面计算书籍的总价:

<body>
    <div id="app">

        <h3>书的总价钱:{{ totalPrice }}</h3>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        data: {
            books: [{
                name: 'Java编程思想',
                price: 99,
                count: 3
            }, {
                name: '编译原理',
                price: 130,
                count: 2
            }, {
                name: 'vue程序设计',
                price: 59,
                count: 1
            }, ]
        },
        computed: {
            totalPrice() {
                return this.books.reduce((total, item) => {
                    return total + item.price * item.count
                }, 0)
            }
        },
    })
</script>

计算属性 是对象属性 而不是 方法
在每个计算属性都包含一个getter和setter 方法,如下面代码

<body>
    <div id="app">
        <button @click="computedSet">给fullName赋值</button>
        <div>全名:{{fullName}}</div>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        data: {
            firstName: 'hello',
            lastName: 'world'
        },
        computed: {
            fullName: {
                set: function(newValue) {
                    console.log("我被调用了!");
                    const names = newValue.split(' ');
                    this.firstName = names[0];
                    this.lastName = names[1];
                },
                get: function() {
                    // return 'abc';
                    return this.firstName + ' ' + this.lastName;
                }

            }
        },
        methods: {
            // 调用了set方法
            computedSet() {
                this.fullName = 'hyb 你好'

            }
        },
    })
</script>

但是,一般情况下,setter方法不常用,经常用到getter().就会将对象属性进行简写

<body>
    <div id="app">
        <div>全名:{{fullName}}</div>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        data: {
            firstName: 'hello',
            lastName: 'world'
        },
        computed: {
            fullName() {
                // return 'abc';
                return this.firstName + ' ' + this.lastName;
            }
        }
    })
</script>

计算属性的缓存
相比较methods计算属性有一个有点就是 缓存
计算属性会进行缓存,如果多次使用时,计算属性只会调用一次

<body>
    <div id="app">
        <div>属性:{{fullName}}</div>
        <div>属性:{{fullName}}</div>
        <div>属性:{{fullName}}</div>

        <div>方法:{{getFullName()}}</div>
        <div>方法:{{getFullName()}}</div>
        <div>方法:{{getFullName()}}</div>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        data: {
            firstName: 'hello',
            lastName: 'world'
        },
        computed: {
            fullName() {
                console.log("调用了计算属性--fullName");
                return this.firstName + ' ' + this.lastName;
            }
        },
        methods: {
            getFullName() {
                console.log("调用了方法:--getFullName");
                return this.firstName + ' ' + this.lastName;
            }
        },
    })
</script>

计算属性的缓存

vue过滤器

过滤器,顾名思义,就是一个数据经过过滤之后出来的东西。
过滤器的用法有多种,在这里介绍一下 在双括号里面使用 和 v-bind中使用

双括号里插值

根据需求的不同有以下几种适配的语法:

  • 多个过滤器: {{val | filterA | filterB | ......}}
  • 多个参数,过滤器第一个参数是|前的值,也可以增加参数:{{val | filter(arg1, arg2,......)}}
  • 多个被过滤的参数:{{val1, val2 | filter}}

多个过滤器
前一个过滤器返回的值,会作为下一个过滤器的参数,一直这样循环往下,直到最后一个过滤器。

<div>{{'2020' | filterA | filterB}}</div>  

filters: {
    filterA(value) {
        return value + "年";
    },
    filterB(value) {
        return value + " 快乐!";
    }
}

多个参数
可以接受额外的其他参数

<div>{{'2020' | filter('1', '20')}}</div>  

filters: {
    filter(value, arg1, arg2) {
        return value + '-' + arg1 + '-' + arg2;
    }
}

多个被过滤的参数
感觉这种方式没必要,用计算属性可以实现

<div>{{'hello', 'world' | filter}}</div>  

filters: {
    filter(value1, value2) {
        return value1 + ' ' + value2;
    }
}

在v-bind表达式中使用

在v-bind表达式中的使用语法和插值的用法一样, <a v-bind:href="vueLink | linkFilter">VueJs官网</a>

事件监听

在开发过程中,经常会有页面交互,比如点击、拖拽、键盘事件等等

在vue中如何监听事件那? =》 通过 v-on指令 其语法糖:v-on:envet == @event

一个简单的例子:

<body>
    <div id="app">
        <div>counter:{{ counter }}</div>

        <button @click="add">增加操作1</button>
        <button @click="counter++">增加操作2</button>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        data: {
            counter: 1,
        },
        methods: {
            add() {
                this.counter++;
            }
        },
    })
</script>

v-on参数

  • 方法本身有一个默认参数,会将原生事件event参数传递
  • 当方法不需要额外参数时,方法后的(),可以不添加
  • 当需要传递其他参数和主动传递event时,可以通过$event传入
<body>
    <div id="app">
        <div>counter:{{ counter }}</div>

        <button @click="add1">增加操作1</button>
        <button @click="add2(2)">增加操作2</button>
        <button @click="add3(3, $event)">增加操作3</button>

    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        data: {
            counter: 1,
        },
        methods: {
            add1() {
                console.log(event);
                this.counter++;
            },
            add2(value) {
                console.log(event);
                this.counter += value;
            },
            add3(value, event) {
                console.log(event);
                this.counter += value;
            }
        },
    })
</script>

v-on 修饰符
在vue中提供了一些事件修饰符,方便处理一些事件

  • .stop 停止冒泡,调用event.stopPropagation()
  • .prevent 阻止默认事件,调用 event.preventDefault()
  • {keyCode|keyAlias} 只有当事件是从特定键触发时才会产生事件
  • .once 只触发一次回掉
    还有几个不常用,就不介绍了,需要时查文档
<style>
    .wraper {
        height: 200px;
        width: 200px;
        background-color: red;
        padding: 50px;
    }
    
    .inner {
        height: 50px;
        width: 50px;
        background-color: blue;
    }
</style>

<body>
    <div id="app">
        <!-- 冒泡 -->
        <div class="wraper" @click="clickWraper">
            <div class="inner" @click.stop="clickInner">

            </div>
        </div>
        <!-- 键修饰符,键别名 -->
        <input type="text" @keyup.enter="onEnter">
        <!-- 键修饰符,键代码-->
        <input type="text" @keyup.13="onEnter">

        <button @click.once="doOnce">once</button>



    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vue = new Vue({
        el: "#app",
        methods: {
            clickWraper() {
                console.log("我是wraper,我被点击了")
            },
            clickInner() {
                console.log("我是inner,我也被点击了")
            },

            onEnter() {
                console.log("键盘输入弹起");
            },

            doOnce() {
                console.log("弹出once次");
            }
        },

    })
</script>

条件判断

v-if

v-if、v-else-if、v-else 这三个指令与JavaScript的条件语句if、 else if、else类似。可以根据表达式的值在DOM中渲染或销毁元素或组件。

<!--HTML-->
<body>
    <div id="app">
        <p v-if="score >= 90">成绩优秀</p>
        <p v-else-if="score>=80">良好</p>
        <p v-else-if="score>=60">及格</p>
        <p v-else>差</p>

    </div>
</body>

<!--js-->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const app = new Vue({
        el: "#app",
        data: {
            score: 98,
        }
    })
</script>

v-if的原理:

  • v-if 后面的条件为false时,对应的元素以及其子元素都不会渲染
  • 也就是说根本不会对应的标签出现在DOM中

**条件渲染案例 **
我们做一个简单的小案例

  • 用户登陆时,可以切换使用用户账号还是邮件地址登陆
  • 如下图场景:
    条件渲染案例
<!--HTML-->
<body>
    <div id="app">
        <span v-if="type == 'userName'">
            <label>用户名:</label>
            <input type="text" placeholder="请输入用户名">
        </span>

        <span v-else>
            <label>邮箱:</label>
            <input type="text" placeholder="请输入邮箱">
        </span>

        <button @click="change">切换方式</button>

    </div>
</body>

<!--JS-->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const app = new Vue({
        el: "#app",
        data: {
            type: "userName"
        },
        methods: {
            change() {
                this.type = this.type === 'userName' ? 'email' : 'userName';

            }
        },
    })
</script>

上边案例有个小问题:
在输入内容的情况下,切换类型时,输入框会有文字,依然显示之前的输入内容,按道理来讲,我们切换到另一个input元素了,在另一个input元素中,我们并没有输入内容,为什么会出现这个问题?

问题解答:
这是因为Vue在进行DOM渲染时,出于性能考虑,尽可能的复用已经存在的元素,而不是重新创建新的元素。在上边的例子中,Vue内部发现原来的input元素不再使用,直接作为else中的input使用

解决方案:
如果我们不希望Vue出现类似的重复利用的情况,我们可以给对应的input添加key,并且保证key值不同

<body>
    <div id="app">
        <span v-if="type == 'userName'">
            <label>用户名:</label>
            <input type="text" placeholder="请输入用户名" key="userName">
        </span>

        <span v-else>
            <label>邮箱:</label>
            <input type="text" placeholder="请输入邮箱" key="email">
        </span>

        <button @click="change">切换方式</button>

    </div>
</body>

v-show

v-show的用法和if非常相似,用于决定一个元素是否渲染

v-if 和 v-show对比
v-if的条件为false,压根不会有对应的元素在DOM中渲染,v-show的条件为false时,仅仅将元素的display属性设置为none

在开发中如何选择?

  • 当需要显示和隐藏操作很频繁时,使用v-show
  • 基本不切换时,使用v-if

循环遍历

v-for 遍历数组

当我们有一组数据需要进行渲染时,我们就可以使用v-for来完成。
格式如下:(item, index) in items 的形式

<div id="app">
    <ul>
        <li v-for="(item, index) in names" :key='index'>{{index}}.{{item}}</li>
    </ul>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const app = new Vue({
        el: "#app",
        data: {
            names: ['孙悟空', '唐僧', '猪八戒', '沙和尚'],
        },

    })
</script>

v-for 遍历对象

<body>
    <div id="app">
        <ul>
            <li v-for="(value, key, index) in obj" :key='index'>{{value}}.{{key}}.{{index}}</li>
        </ul>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const app = new Vue({
        el: "#app",
        data: {
            obj: {
                name: '前端阿彬',
                age: 18,
                height: 1.80
            },
        },

    })
</script>

组件的key属性
官方推荐我们使用v-for时,给对应的元素添加:key属性

当某一层有很多的相同的节点时,我们插入一个新的节点
我们希望在B和C之间添加F,Diff算法默认执行起来是这样的,把C更新成F,D更新成C,E更新成D,最后再插入E。

当我们给每个节点添加一个唯一标识key时,Diff算法就可以正确的识别此节点,找到正确的位置插入新的节点。

所以总结一句话就是。key的作用主要是高效的更新虚拟DOM。

组件的key属性

监测数组更新
vue中包含了一组观察数组编译的方法,使用他们改变数组也会触发视图的更新

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

购物车的例子

  • 总价计算的位置,computed这个位置
  • 在表单中 购买数量加减 index的传递,不能直接将count传递
  • filters的使用 代码:
    HTML 代码
<style>
    table {
        border-collapse: collapse;
        text-align: center;
    }
    
    table,
    tr,
    td,
    th {
        border: 1px solid #000000;
    }
    
    td,
    th {
        padding: 5px 20px;
    }
</style>

<body>
    <div id="app">
        <table>
            <tr>
                <th></th>
                <th>书籍名称</th>
                <th>出版日期</th>
                <th>价格</th>
                <th>购买数量</th>
                <th>操作</th>
            </tr>
            <tr v-for="(item, index) in books" :key="index">
                <td>{{index + 1}}</td>
                <td>{{item.name}}</td>
                <td>{{item.publish}}</td>
                <td>{{item.price | priceFilter}}</td>
                <td>
                    <button @click="decrease(index)" :disabled='item.count == 1'>-</button>
                    <span>{{item.count}}</span>
                    <button @click="add(index)">+</button>
                </td>
                <td>
                    <button @click="remove(index)">移除</button>
                </td>
            </tr>
        </table>
        <span>总价: {{ total | totalFilter }}</span>
    </div>
</body>

JS 代码

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vm = new Vue({
        el: "#app",
        data: {
            books: [{
                name: '《算法导论》',
                publish: '2006-9',
                price: '85.00',
                count: 1,
            }, {
                name: '《UNIX编程艺术》',
                publish: '2006-2',
                price: '59.00',
                count: 1,
            }, {
                name: '《编程珠玑》',
                publish: '2006-10',
                price: '39.00',
                count: 1,
            }, {
                name: '《代码大全》',
                publish: '2006-3',
                price: '128',
                count: 1,
            }]


        },
        methods: {
            decrease(index) {
                this.books[index].count--;
            },
            add(index) {
                this.books[index].count++;
            },
            remove(index) {
                this.books.splice(index, 1);
            }

        },

        computed: {
            total() {
                var total = this.books.reduce(function(total, item) {
                    return total + item.price * item.count;
                    console.log('total:', total);
                    console.log('item:', item);
                }, 0)

                return total;

            }

        },
        filters: {
            totalFilter(val) {
                return val.toFixed(2) + "元";
            },

            priceFilter(val) {
                return "$" + parseFloat(val).toFixed(2);
            }
        }

    })
</script>

v-model

v-model指令实现表单元素和数据的双向绑定

v-model其实就是一个语法糖,它的背后本质上是包含两个操作:

  1. v-bind绑定一个属性
  2. v-on指令给当前元素绑定input事件
<div id="app">
    <input type="text" :value="message" @input="changeValue">
    <h2>{{ message }}</h2>
</div>  

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vm = new Vue({
        el: "#app",
        data: {
            message: 'vfe',
        },
        methods: {
            changeValue() {
                this.message = event.target.value;
            }

        },
    })
</script>

表单绑定v-model : juejin.cn/post/684490…

v-model修饰符

  • lazy 修饰符可以让数据在失去焦点或者回车时才会更新
  • number 默认情况下,我们输入的不论字母还是数字,都会被当作字符串类型进行处理,如果希望输入的是数字类型时,number修饰符可以在输入框输入的内容自动转化成数字类型 ,感觉用处不大
  • trim 过滤输入内容左右两边的空格
<div id="app">
    <!-- lazy -->
    <input type="text" v-model.lazy="message">
    <h2>message: {{ message }}</h2>

    <input type="text" v-model.trim="trimVal">
    <h2>trimVal: {{ trimVal }}</h2>

    <input type="text" v-model.number="numberVal">

</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    const vm = new Vue({
        el: "#app",
        data: {
            message: '',
            trimVal: '',
            numberVal: '',
        }
    })
</script>

组件化开发

vue组件化思想

如果将一个页面所有的逻辑全部放在一起处理起来变得非常复杂,而且不利于后续的管理以及扩展,但是如果,我们将页面拆成一个个小的功能模块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易。

vue组件化思想

组件化思想的应用

  • 尽可能将页面拆成一个个小的、可复用的组件
  • 这样可以让我们的代码更加方便组织和管理,并且扩展性也很强

注册组件的基本步骤

组件使用分为三个步骤:

  • 创建组件构造器
  • 注册组件
  • 使用组件
    注册组件的基本步骤
<body>
    <div id="app1">
        <!-- 3.使用组件 -->
        <my-cpn></my-cpn>
    </div>

    <div id="app2">
        <!-- 3.使用组件 -->
        <my-cpn></my-cpn>

    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    // 1.创建组件构造器  
    const myComponent = Vue.extend({
        template: `
            <div>
                <h2>组件标题</h2>
                <p>我是组件中的一个段落内容</p>
            </div>
        `
    })

    // 2.注册组件,并且定义组件标签的名称
    Vue.component('my-cpn', myComponent);

    const vm1 = new Vue({
        el: "#app1",
        data: {}
    })

    const vm2 = new Vue({
        el: "#app2",
        data: {}
    })
</script>

注册组件步骤解析

  1. Vue.extend()
  • 调用Vue.exten()创建的是一个组件构造器
  • 通常在创建组件构造器时,传入template代表我们自定义组件的模板
  • 该模板就是在使用到组件的地方,要显示的HTML代码
  • 事实上,这种写法在文档中几乎看不到,会直接使用接下来的语法糖,但是这种方式是学习后面方式的基础
  1. Vue.component()
  • 调用Vue.component()是将刚才的组件构造器注册为一个插件,并且给它起一个组件的标签名称
  • 所以需要传递两个参数:1. 注册组件的标签名 2.组件构造器
  1. 组件必须挂载在Vue实例下,否则不会生效
  • <my-cpn></my-cpn>

全局组件和局部组件

上边的例子 使用Vue.component()注册组件时,组件的注册是全局的,这就意味着该组件可以在任意Vue实例下使用。

如果注册的组件是挂载在某个实例下的,就是局部组件,只能在该实例下使用。

<body>
    <div id="app1">
        <!-- 3.使用组件 -->
        <my-cpn></my-cpn>
    </div>

    <div id="app2">
        <!-- 3.使用组件 ,因为是局部组件,这个不会被渲染-->
        <my-cpn></my-cpn>

    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    // 1.创建组件构造器  
    const myComponent = Vue.extend({
        template: `
            <div>
                <h2>组件标题</h2>
                <p>我是组件中的一个段落内容</p>
            </div>
        `
    })

    const vm1 = new Vue({
        el: "#app1",
        // 2.注册局部组件,并且定义组件标签的名称
        components: {
            'my-cpn': myComponent
        }
    })

    const vm2 = new Vue({
        el: "#app2",
        data: {}
    })
</script>

父组件和子组件

如何组成父子组件的层级关系

  • 创建子组件的构造器
  • 将子组件构造器注册到父组件的components中,注册为子组件
  • 在父组件中使用子组件
<body>
    <div id="app">
        <!-- 3.使用组件 -->
        <fa-cpn>

        </fa-cpn>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    // 1.1 创建子组件构造器 
    const sonComponent = Vue.extend({
        template: `
            <div>
                <h2>子组件</h2>
            </div>
        `
    })

    // 1.创建父组件构造器  
    const fatherComponent = Vue.extend({
        template: `
            <div>
                <h2>父组件</h2>
                <p>我是组件中的一个段落内容</p>
                // 1.3 使用子组件
                <son-cpn></son-cpn>
            </div>
        `,
        // 1.2 注册局部子组件,并且定义组件标签的名称
        components: {
            'son-cpn': sonComponent
        }
    })

    const vm = new Vue({
        el: "#app",
        // 2.注册局部组件,并且定义组件标签的名称
        components: {
            'fa-cpn': fatherComponent
        }
    })
</script>

注册组件的语法糖

上面注册组件的方式,可能会有些繁琐

  • Vue为了简化这个过程,提供了注册的语法糖
  • 主要是省去调用Vue.extend()的步骤,而是直接使用一个对象来替代

全局组件的语法糖

全局组件的语法糖

局部组件的语法糖

局部组件的语法糖

模板的分离写法

通过语法糖简化了vue组件的注册过程,还有一个地方的写法比较麻烦,就是template模板写法。

如果能将HTML分离出来,然后挂载到响应的组件中,结构必然变的非常清晰

Vue提供了两种方案定义HTML模板内容:

  • 使用<script>标签
  • 使用<template>标签

使用<script>标签

<div id="app">
    <my-cpn></my-cpn>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script type="text/x-template" id="myCom">
    <div>
        <h1>模板分离</h1>
        <div>我是一个组件</div>
    </div>
</script>
<script>
    const vm = new Vue({
        el: "#app",
        components: {
            'my-cpn': {
                template: '#myCom'
            }
        }
    })
</script>

使用<template>标签

<div id="app">
    <my-cpn></my-cpn>
</div>  

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<template id="myCom">
    <div>
        <h1>模板分离</h1>
        <div>使用template,我是一个组件</div>
    </div>
</template>
<script>
    const vm = new Vue({
        el: "#app",
        components: {
            'my-cpn': {
                template: '#myCom'
            }
        }
    })
</script>

组件可以访问Vue实例数据吗?

组件是单独功能模块的封装
单独模块有属于自己的HTML模板,那也应该有属性,保存在自己data中

那我们先来测试一下,vue组件中能否访问Vue实例中的data中的数据?

<body>
    <div id="app">
        <my-cpn></my-cpn>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<template id="myCom">
    <div>
        <h1>模板分离</h1>
        <div>使用template,我是一个组件</div>
        <div>访问:{{message}}</div>
    </div>
</template>
<script>
    const vm = new Vue({
        el: "#app",
        data: {
            message: 'vue实例的数据',
        },
        components: {
            'my-cpn': {
                template: '#myCom'
            }
        }
    })
</script>

在vue组件并不能访问vue实例data中的属性。报错:Property or method "message" is not defined on the instance but referenced during render.

那么组件的数据存放在何处?
组件中也有data属性,只是这个data属性必须是函数,而这个函数返回一个对象,对象内部保存着数据

<body>
    <div id="app">
        <my-cpn></my-cpn>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<template id="myCom">
    <div>
        <h1>模板分离</h1>
        <div>使用template,我是一个组件</div>
        <div>访问内部message数据:{{message}}</div>
    </div>
</template>
<script>
    const vm = new Vue({
        el: "#app",
        components: {
            'my-cpn': {
                template: '#myCom',
                data() {
                    return {
                        message: "组件内部message数据",
                    }
                }
            },

        }
    })
</script>

data在组件中为什么是一个函数?

  1. 首先,如果不是函数,Vue直接就会被报错
  2. Vue让每个组件都返回一个新的对象,因为如果是同一个对象的,组件多次调用会受影响。

父子组件通信

子组件是不能引用父组件和Vue实例中的实例

但是在实际开发中,有些数据是从上至下的:

  • 比如我们在首页请求的数据,有些数据是一些子组件来展示,那么需要将数据传递给子组件,让其自己展示

父组件或vue实例如何与子组件通信?

父子通信

  • 通过props向子组件传递数据
  • 通过事件向父组件或vue实例发送信息

父组件向子组件传递数据
在组件中,使用props来声明需要从从父组件或vue实例传递的数据

在接收数据时,props有两种方式:

  • 方式一:字符串数组,数组中的元素就是传递时的名称
  • 方式二:对象,对象方式有两点好处
    第一:设置传递的类型
    第二:可设置默认值

方式一:使用数组方式接收

<body>
    <div id="app">
        <!-- 1.传递给子数组数据 -->
        <my-cpn :message='message'></my-cpn>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<template id="myCom">
    <div>
        <h1>接收数据</h1>
        <div>子组建,接收vue数据为:{{message}}</div>
    </div>
</template>
<script>
    const vm = new Vue({
        el: "#app",
        data: {
            message: "数组方式接收message数据",
        },
        components: {
            'my-cpn': {
                template: '#myCom',
                // 2.使用props数组方式接收
                props: ['message']
            },

        }
    })
</script>

方式二:使用对象方式接收数据
当我们需要对传递的数据进行验证时,就需要对象写法,验证都支持如下一些数据类型,当然当我们有自定义构造函数时,验证也支持自定义的类型。

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol
    传递

自定义验证对象

自定义验证对象

注意:注意驼峰写法

子组件向父组件传递数据
如果子组件传递数据或信息到父组件中,这时候就需要通过 自定义事件 来完成。

自定义事件的流程:

  • 在子组件中,通过$emit()来触发事件
  • 在父组件中,通过v-on来监听子组件事件
    <body>
        <div id="app">
            <!-- 1.传递给子数组数据 -->
            <my-cpn @add-counter='addCounter'></my-cpn>
    
            <h2>total的值</h2>
            <div>total: {{total}}</div>
        </div>
    </body>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    
    <!-- 子元素的模板 -->
    <template id="myCom">
        <div>
            <h1>子组件向父组件传递数据</h1>
            <h2>couter数据:{{counter}}</h2>
            <button @click="emitCouter">将couter递加传递到vue实例中</button>
        </div>
    </template>
    <script>
        const vm = new Vue({
            el: "#app",
            data: {
                total: 0,
            },
            methods: {
                addCounter: function(value) {
                    console.log(value);
                    this.total = value;
    
                }
            },
            components: {
                'my-cpn': {
                    template: '#myCom',
                    data() {
                        return {
                            counter: 0,
                        }
                    },
                    methods: {
                        emitCouter: function() {
                            this.counter++;
                            this.$emit('add-counter', this.counter);
                        }
                    },
    
    
                },
    
            }
        })
    </script>
    

非父子组件通信

在Vue1.x的时候,通过dispatch和broadcast完成,但是在Vue2.x中被取消了。

在vue2.x中,通过中央事件总线这种方案来完成,也可以通过Vuex来完成

父子组件的访问

父组件访问子组件

有时候我们需要父组件直接访问子组件,使用两种方式:

  • $children
  • $refs (reference 引用)

使用$children

使用$children

通过$children获取的是子组件的数组,只能根据索引值来获取子组件,不方便获取特定的子组件,因此通过 $refs可以很方便获取某个子组件

$refs的使用
$refs指令和ref是在一起使用的,首先,通过ref给某个子组建绑定一个特定的ID。然后,通过this.$refs.ID就可以访问该组件。

refs的使用1
refs的使用2

子组件访问父组件

子组件直接访问父组件时,可以直接通过$parent.

子组件访问父组件

虽然可以通过$parent访问父组件,但是尽量不要使用:

  • 直接访问父组件,耦合度太高
  • 如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性,往往会引起问题
  • 如果通过$parent更改parent组件的状态,将导致不便于维护组件状态,也不利于调试和维护

组件中访问根组件vue实例
通过$root访问vue实例

slot

slot翻译为插槽,在生活中很多地方都有插槽,电脑USB插槽,电源插板的插槽。这些插槽的目标是让原来的设备具备更多的扩展性。比如电脑的usb插槽可以插入:鼠标、键盘、音响、U盘等等

组件的插槽也是让我们封装的组件更有扩展性,让使用者决定组件内部到底展示什么。比如导航栏,在软件中有各式各样的导航栏,我们就可以插槽来实现。

如何使用slot封装组件?
总结来说:抽取共性,保留不同
将共性抽取到组件中,将不同暴露为插槽。一旦我们预留了插槽,可以根据使用者自己的需求,决定插槽中插入什么内容。

slot的基本使用

如何在组件中使用slot?
在子组件中,使用特殊的元素<slot>就为子组建开启了一个插槽
该插槽插入什么内容取决域父组件如何使用

slot的基本使用

具名插槽slot

在一个组件中可能有多个插槽,那么我们怎么将元素插到指定的插槽元素中?这就需要具名插槽。

如何使用具名插槽?

  • 给slot添加一个name属性
  • 给插入元素添加slot等于name的属性
    具名插槽

编译作用域

父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。

如下面的例子:

编译作用域
my-cpn 组件可以显示。

我们在使用<my-cpn>的时候,整个组件的使用过程在父组件中使用,那么他的作用域就是父组件,使用的属性也是父组件的属性。因此isShow使用的是vue实例的属性,而不是子组件的属性。

作用域插槽

作用域插槽的作用:
父组件替换插槽的标签,但是数据由子组件提供

如下一个需求:如果在子组件中有一组数据,pLanguages:["C", "C++", "python", 'JS'],我们希望在不同的页面展示不同的形式。

<body>
    <div id="app">
        <my-com></my-com>

        <my-com>
            <!-- template是vue2.5版本一下的用法,大于2.5的版本可以用任意标签 -->
            <!-- 使用用 slot-scope接收子组件插槽传递的数据-->
            <template slot-scope="slot">
                <span>{{slot.data.join(" - ")}}</span>
            </template>
        </my-com>
    </div>

</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<template id="myCom">
    <div>
        <h1>myCom组件</h1>
        <!-- 在slot中传递给父组件数据,可以任意命名,在这里命名data -->
        <slot :data="pLanguages">
            <ul>
                <li v-for="(item, index) in pLanguages" :key="index">{{item}}</li>
            </ul>
        </slot>
    </div>
</template>
<script>
    const vm = new Vue({
        el: "#app",
        components: {
            'my-com': {
                template: "#myCom",
                data() {
                    return {
                        pLanguages: ["C", "C++", "python", 'JS'],
                    }
                },
            }

        }
    })
</script>

模块化开发

为什么需要模块化开发?
在开发早期,js作为脚本语言,做一些简单的表单验证,代码还很少。随着ajax异步请求的出现,慢慢形成了前后端的分离,客户端需要完成的事情越来越多,代码量剧增,我们通常将代码组织在多个js文件中进行维护。但是这种维护方式,会有很多问题,比如 全局变量同名问题

全局变量同名

另外,这种编码方式,对于js引用也是有一定依赖关系。

匿名函数解决问题

上边全局变量问题可以使用匿名函数解决。在aaa.js文件中,我们使用匿名函数

(function(){
    var flag = true;
})()

但是,另一个问题就是我们如何在main.js中利用变量flag,因为flag是一个局部变量。

我们可以将需要暴露到外边的变量,作为一个模块作为出口。来看下面的代码:
aaa.js

var moduleA = (function() {
    var obj = {};
    obj.flag = true;

    obj.foo = function(info) {
        console.log(info);
    }

    return obj
})()

main.js

if (moduleA.flag) {
    console.log("小明是个天才!");

}

moduleA.foo("简单的模块还1");

上边的代码,我们在在匿名函数中定义个一个对象,给对象添加需要暴露到外边的属性和方法,将这个对象返回。在main.js中,使用属于自己的属性和方法。

以上便是基础的模块封装,不过前端模块开发已经有很多规范,和对应的实现方案。常见的模块化规范:
CommonJS、AMD、CMD、和ES6的模块化方案

模块化开发规范

CommonJS
模块化有两个核心:导出和导入

CommonJS的导出:

module.exports = {
    flag: true,  
    test(a, b) {
        return a + b;
    }
}

CommonJS的导入

let {test, flag} = require('./moduleA');  

<!--等同于-->  
let _ma = require("./moduleA");  
let test = _ma.test;
let flag = _ma.flag;

ES6

在ES6模块化方案中,主要使用两个指令:

  • export 用于导出模块对外提供的接口,以及 export default
  • import指令主要用于加载对应的模块

export
使用export导出变量时,有两种方式导出,第一种是一个一个导出,如下代码:

export let name = 'huyingbin';
export let age = 18;
export let height = 1.88  

export function test(content) {
    console.log(content);
}

export class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    run() {
        console.log(this.name + "在奔跑!");
    }

}

另一种方式是通过统一导出的模式,如下:

let name = "huyingbing";
let age = 18;
let height = 1.88;

function test(content) {
    console.log(content);
}

class Person {
    constructor(name, age) {
         this.name = name;
         this.age = age;
    }

     run() {
         console.log(this.name + "在奔跑");
     }
 }
<!--统一导出-->
export { name, age, height, test, Person } 

import
因为我们这里是通过js文件直接在浏览器中使用,在引入文件中要加入 type="module"
<script src="./main.js" type="module"></script>

导入:

import { name, age, height } from './es.js'
console.log(name, age, height);

如果将某个模块的所有信息都导入,一个个导入显然必要麻烦,我们可以通过*符号导入所有的信息,然后命名一个别名方便信息的使用

import * as moduleInfo from './es.js'

console.log(moduleInfo.name, moduleInfo.age);

export default
某些情况下,一个模块包含某个功能,我们不希望给这个功能命名,让导入这自己命名,这个时候我们就可以是哟个 export default
首先在一个文件中通过export default导出

export default function() {
    console.log("default function");
}

在main.js中,导入

<!--注意myfun可以随意命名-->
import myfun from './es.js'

myfun();

另外,需要注意:
export default 在一个模块中,只能使用一次

webpack详解

什么是webpack

webpack是一个现代Javascript应用的静态模块打包工具

一下从两点解释一下:模块 和 打包

前端模块化:

  • 针对目前的模块化规范:AMD、CMD、CommonJS、ES6,除ES6方案外,想要进行模块化开发需要借助一些工具,webpack就是其中一个支持模块化开发的工具
  • 一个项目有很多模块,各个模块之间的依赖关系,构成了一个复杂的依赖关系网,webpack能处理复杂的依赖关系,进行整体打包
  • 在项目中不仅有js文件,项目的中的CSS,图片等在webpack也可作为模块进行打包
  • 总之来说,webpack中模块化的概念的核心是让我们能进行模块化开发,并且能够处理模块间的依赖关系

打包:

  • 将各种资源进行打包合并成一个或多个包(Bundle)
  • 并且在打包的过程中还可以对资源进行处理,比如压缩图片,将SCSS转化为css,将ES6语法转化为ES5,将TypeScript转化为javaScript

Webpack VS grunt/gulp

grunt/gulp的核心是task。我们可以配置一系列的task, 并且定义task要处理的事物(ES6,tS转化为ES5,sass转化为css等等)。之后让grunt/gulp执行这些任务,让整个流程自动化,因此 grunt/gulp也被称为前端自动化任务管理工具。

我们看一个gulp的task:

件转化为ES5的

上面的任务是将src文件下的所有js文件转化为ES5的语法,并最终出在dist文件下。

webpack-Demo演示

安装webpack

在安装webpack之间要安装nodejs,因为我们要利用他的包管理工具npm安装webpack

全局安装webpack
npm install webpack -g

查看安装webpack的版本
webpack -v

不同版本的webpack,输入的命令有所不同,本次安装的webpack版本是v4.41.5

项目准备

创建以下文件夹及文件

  • dist文件夹:用于存放打包后的文件
  • src文件夹: 存放源代码
    main.js文件: 项目入口文件
    mathUtil.js文件:数学工具文件
  • index.html文件:浏览器展示的首页html
    项目结构

mathUtil.js文件代码:

function add(num1, num2) {
    return num1 + num2;
}

function mul(num1, num2) {
    return num1 * num2;
}

module.exports = {
    add,
    mul
}

main.js文件代码:

const math = require('./mathUtils.js');

console.log(math.add(10, 12));

js文件打包

上边的代码使用了模块化的开发方式,可否通过直接引入的方式,不可以,主要有两点原因:

  • 浏览器不能识别里面的模块化代码
  • 在真实项目中有很多js代码,一个个引入也非常麻烦,后期也不方便管理

我们可以使用webpack,帮助我们处理其中的依赖关系,并且将多个文件打包成一个,我们引入打包后的js文件即可

如何打包?使用以下指令即可:
webpack ./src/main.js -o ./dist/bundle.js --mode development

将打包后的bundle.js文件引入index.html即可:

<body>
    <h1>这里是index页面</h1>

</body>
<script src="./dist/bundle.js"></script>  

webpack基本配置

每次运行打包指令时都需要带上入口和出口文件作为参数,着实麻烦,因此我们是否可以将这两个参数写到配置中,在运行时,直接读取。
因此,我们可以创建一个webpack.config.js文件。

const path = require('path')

module.exports = {
    entry: './src/main.js',

    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    }
}

接下来,如需打包,我们只需输入以下指令:
webpack --mode development

通常来说一个项目都有自己局部的webpack,因为一个项目可能依赖特定版本的webpack,使用全局webpack可能与项目不兼容。安装局部webpack:
npm install webpack --save-dev

--save-dev是开发时依赖,项目上线后就不需要依赖

通过局部webpack运行上边项目:
node_modules/.bin/webpack --mode development

上边这种运行项目的方式也是有点麻烦,我们可以package.json文件中定义一些命令。
创建package.json文件: npm init

然后一系列操作,会出现package.json 文件

{
    "name": "webpacktest",
    "version": "1.0.0",
    "description": "",
    "main": "webpack.config.js",
    "dependencies": {
        "webpack-cli": "^3.3.10",
        "webpack": "^4.41.5"
    },
    "devDependencies": {},
    "scripts": {
    
    },
    "author": "",
    "license": "ISC"
}

我们在"scripts"中定义:

"scripts": {
    "build": "webpack --mode development"
 },

当我们执行
npm run build后,会找到package.json中的scripts的脚本的"build": "webpack --mode development",然后找到本项目组中的node_modules/.bin 路径下的webpack,若没找到就会去找全局变量的webpack

css-loader

什么是loader?
webpack打包代码时处理css文件,.vue文件,图片时,需要不同的loader协助webpack来工作

loader的使用方法:

  • 通过npm 安装不同的loader
  • 在webpack.config.js的modules关键字下进行配置

准备CSS文件
在src目录中,创建css文件夹,用于存放样式文件,创建normal.css文件。创建js文件夹,用于存放项目中的js文件。

在normal.css文件中,非常简单,设置body的背景为红色red.

body {
    background-color: red;
}

在入口文件main.js中引入样式文件,webpack 会从入口查找依赖的其他文件。

const math = require('./js/mathUtils.js');

console.log(math.add(10, 12));

// 引入css文件
require('./css/normal.css')

要想成功打包CSS代码,需要安装css-loader,style-loader,css-loader用于加载css文件;style-loader用于将样式嵌入DOM上。

npm install --save-dev css-loader
npm install style-loader --save-dev

按照官方要求配置webpack.config.js文件

    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },

    module: {
        rules: [{
            test: /\.css$/,
            use: ['style-loader', 'css-loader']
        }]
    },

注意,style-loader需要在css-loaser前面,因为webpack在读取使用的loader时,是从右到左的顺序。

最后,运行npm run build 重新打包项目,样式就可以起作用了。

less文件处理

在项目中使用less对样式进行设置,我们创建一个specill.less:

@fontSize: 30px;
@fontColor: blue;

body {
    color: saturate(@fontColor, 5%);;
    font-size: @fontSize;
}

在入口main.js中引入

require('./css/specill.less')

处理less文件时,首先,需要安装less-loader

npm install --save-dev less-loader less

然后,按照官方要求配置webpack.config.js文件:

module: {
    rules: [{
            test: /\.less$/,
            use: [{
                loader: "style-loader" // creates style nodes from JS strings
            }, {
                loader: "css-loader" // translates CSS into CommonJS
            }, {
                loader: "less-loader" // compiles Less to CSS
            }]
        }
    ]
},

最后,使用指令 npm run build 重新打包一下。

处理图片

准备两张图片,一张小于8k的图片small.jpg,一张相比较大的背景图片big.jpg。

在normal.css文件中简单应用:

body {
    background: url(../img/small.jpg);
}

首先安装url-loader

npm install --save-dev url-loader

配置webpack.config.js文件

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192
            }
          }
        ]
      }
    ]
  }
}

使用 npm run build 指令重新打包

此url-loader有个limit配置项,当图片小于此数值时(8k),会使用此loader对图片进行base64编码并显示。

base64编码

当图片大于limit设定的值时,url-loader就不能处理,需要file-loader来处理,安装:

npm install --save-dev file-loader

这时需要在webpack.config.js文件的output添加publicPath路径,引用地址方能正确。

output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    publicPath: 'dist/'
},

这时,打包的图片文件名很长,我们可以设置webpack.config.js文件url-loader的options一些设置项。

options: {
    limit: 8192,
    name: 'img/[name].[hash:8].[ext]'
}

解释options中一些选项:

  • img:文件要打包的文件夹
  • name:获取图片原本的名字,放在该位置
  • hash:8:为防止图片名字冲突,依然使用hash,但是只保留8位
  • ext:图片的原来的扩展名

ES6语法的处理

此时打包后的文件,ES6语法并没有转化为ES5语法,这就意味着对一些ES6语法不支持的浏览器不能运行程序,因此需要babel-loader将ES6语法专为ES5。
安装

npm install babel-loader@8.0.0-beta.0 @babel/core @babel/preset-env webpack

配置webpack.config.js文件

{
  test: /\.js$/,
  exclude: /(node_modules|bower_components)/,
  use: {
    loader: 'babel-loader',
    options: {
      presets: ['@babel/preset-env']
    }
  }
}

引用vue.js

安装vue

npm i vue -S

写一些简单的vue代码
index.html

<div id="app">
    {{ message }}
</div>

main.js

import Vue from 'vue'
const vm = new Vue({
    el: "#app",
    data: {
        message: '使用vue插件'
    },
})

修改webpack.config.js文件

module.exports = {
    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.esm.js'
        }
    }
}

使用npm run build 打包运行

template 和 el的关系

上面有个问题,我们想将data的数据显示在界面上,就必须操作index.html文件。如果后面使用自定义的组件,也需要修改index.html文件。如何不需要频繁操作index.html文件?

定义template属性
在前面的例子中,我们定义了el 属性,用于绑定index.html的#app元素,让vue实例管理它里面的元素。这里我们将div中内容删除,保留一个id为div的元素。我们使用template元素:

const vm = new Vue({
   el: "#app",
   template: ' <div id="app">{{ message }}</div>',
   data: {
       message: '使用vue插件'
   },
})

运行打包后,和上边的结构是一样滴

el 和 template 什么关系?
在原来的例子中,el 用于指定vue要管理的DOM,可以帮助我们解析指令,事件监听等等。如果vue实例中同时指定tenplate,那么template模板内容会替换挂载对应的el的模板。

在实例中书写template模板会很麻烦,后面我们会将template模板抽离,分成三部分书写:template、script、style书写,样式更清晰

vue组件化开发

创建App.js文件,自定义App组件

// 定义App组件
const App = {
    template: '<div>{{name}}</div>',
    data() {
        return {
            name: "我是app组件"
        }
    }
}

export { App }

在main.js中导入,并且使用App组件

import { App } from './js/App.js' 

const vm = new Vue({
    el: "#app",
    template: `
        <div id="app">
            {{ message }}
            <App></App>
        </div>`,
    data: {
        message: '使用vue插件'
    },
    components: {
        App,
    }
})

一个组件以js的方式创建和使用及其不方便:

  • 编写template模块比较麻烦
  • 如果需要样式,写在哪里感觉都不太方便和直观

将上面例子,接下来将以.vue的方式创建组件,将template模板抽离,分成三部分书写:template、script、style书写

创建App.vue组件

<template>
    <div class="App"> 
        {{name}}
    </div>
</template>  

<script>
export default {
    data() {
        return {
            name: '我是APP组件'
        }
    }
}
</script>

<style scoped>
.App {
    color: aliceblue;
}

</style>

在main.js引入组件


import Vue from 'vue'
import App from './js/App.vue'

const vm = new Vue({
    el: "#app",
    template: `
        <div id="app">
            {{ message }}
            <App></App>
        </div>`,
    data: {
        message: '使用vue插件'
    },
    components: {
        App,
    }
})

安装vue-loader以及vue-template-compiler

npm install vue-loader vue-template-compiler --save-dev

配置webpack.config.js

module: {
        rules: [
            {
                test: /\.vue$/,
                use: ['vue-loader']
            }
        ]
    },

注意
Vue-loader在15.*之后的版本都是 vue-loader的使用都是需要伴生 VueLoaderPlugin的,在webpack.config.js中加入

const VueLoaderPlugin = require('vue-loader/lib/plugin');  

plugins: [
    // make sure to include the plugin for the magic
    new VueLoaderPlugin()
],

运行 npm run build重新打包,即可

一些插件的使用

webpack的插件,就是对webpack的功能进行扩展,比如打包优化,文件压缩等等

loader和plugin的区别

  • loader 主要用于转换某些类型的模块,它是一个转换器。
  • plugins 是插件,是对webpack本身的扩展,是一个扩展器

plugin的使用过程

  • 通过npm安装某些plugins
  • 在webpack.config.js 的plugins中配置插件。

**添加版权的plugin ** BannerPlugin属于webpack自带的插件,用于对打包的文件添加声明

修改webpack.config.js文件

const webpack = require('webpack')

plugins: [
    new webpack.BannerPlugin('版权归hyb所有!')
],

打包html的plugin
目前index.html是放在项目的根目录下,但是真实的项目发布时,发布的是dist文件夹中的内容,目前dist文件夹下并无index.html

所以,将index.html打包到dist文件夹下,这时候就可以使用html-webpack-plugin插件

html-webpack-plugin插件主要做了:

  • 自动生成index.html模板
  • 将打包的js文件,自动通过script标签插入到body中

安装html-webpack-plugin插件

npm install html-webpack-plugin --save-dev

修改webpack.config.js的plugins

const htmlWebpackPlugin = require('html-webpack-plugin')

plugins: [
    new htmlWebpackPlugin({
        template: 'index.html'
    })
],

注意,删除之前在output中添加的publicPath属性

js压缩的Plugin
在项目发布之前,我们必然对js文件进行压缩处理

这里我们使用uglifyjs-webpack-plugin,对打包的js压缩

安装uglifyjs-webpack-plugin

npm install uglifyjs-webpack-plugin@1.1.1 --save-dev

修改webpack.config.js

const uglifyJsPlugin = require('uglifyjs-webpack-plugin')
 
plugins: [
    new uglifyJsPlugin()
],

搭建本地服务
webpack提供了一个可选的本地开发服务器,可以实现让浏览器自动刷新显示我们修改后的结果,这个本地服务器基于node.js搭建,内部使用express框架。

安装:

npm install --save-dev webpack-dev-server

配置webpack.config.js中devserver下的一些属性

devServer: {
    contentBase: './dist',
    inline: true
},

contentBase:为哪个文件夹提供本地服务
port:端口号
inline:页面是否实时刷新

配置package.json下scripts的启动项

"scripts": {
    "dev": "webpack-dev-server --open"
},

启动打包 npm run dev后,改动代码,保存后页面就会刷新

配置文件分离
在开发中,为了将开发配置和发布配置分开,将webpack.config.js文件拆成三个文件,分别是:公共文件base.config.js,开发配置dev.config.js,和发布配置文件pro.config.js

首先,需要安装合并配置的插件:

npm install webpack-merge --dev-save

base.config.js文件
重点说一下,output中的path属性,要更改成:path: path.resolve(__dirname, '../dist'),

const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const htmlWebpackPlugin = require('html-webpack-plugin')
const uglifyJsPlugin = require('uglifyjs-webpack-plugin')
const webpack = require('webpack')

module.exports = {
    entry: './src/main.js',

    output: {
        path: path.resolve(__dirname, '../dist'),
        filename: 'bundle.js',
        // publicPath: 'dist/'
    },
    module: {
        rules: [{
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            },
            {
                test: /\.less$/,
                use: [{
                    loader: "style-loader" // creates style nodes from JS strings
                }, {
                    loader: "css-loader" // translates CSS into CommonJS
                }, {
                    loader: "less-loader" // compiles Less to CSS
                }]
            },
            {
                test: /\.(png|jpg|gif)$/,
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 8192,
                        name: 'img/[name].[hash:8].[ext]'
                    }
                }]
            },
            {
                test: /\.js$/,
                exclude: /(node_modules|bower_components)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }
            },
            {
                test: /\.vue$/,
                use: ['vue-loader']
            }
        ]
    },

    plugins: [
        // make sure to include the plugin for the magic
        new VueLoaderPlugin(),
        new webpack.BannerPlugin('版权归hyb所有!'),
        new htmlWebpackPlugin({
            template: 'index.html'
        }),
        // new uglifyJsPlugin()
    ],

    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.esm.js'
        }
    }
}

开发配置文件dev.config.js

const baseConfig = require('./base.config')
const webpackMerge = require('webpack-merge')
module.exports = webpackMerge(baseConfig, {
    devServer: {
        contentBase: './dist',
        inline: true
    }
})

发布配置文件pro.config.js

const baseConfig = require('./base.config')
const uglifyJsPlugin = require('uglifyjs-webpack-plugin')
const webpackMerge = require('webpack-merge')

module.exports = webpackMerge(baseConfig, {
    plugins: [
        new uglifyJsPlugin()
    ]
})

package.json文件修改

"scripts": {
    "build": "webpack --config ./build/pro.config.js --mode development",
    "dev": "webpack-dev-server --open --config ./build/dev.config.js"
},

Vue CLI(version > 3)

CLI (Command-Line Interface),命令行界面,俗称脚手架。

在使用Vue.js开发大型应用时,我们需要考虑目录结构,使用各种loader解析和转换文件,热加载等等事情。如果在每个项目中都要手动去完成这些工作,就会有很大的时间成本,所以我们会使用脚手架去帮助我们干这些事情。

使用Vue CLI脚手架可以快速搭建Vue开发环境以及对应的webpack配置。

Vue CLI 3.X之后的版本与2.x版本有很大的区别

  • Vue ClI 3+(3版本之后的版本)的设计原则是“0配置”,移除根目录下的build和config的目录
  • 新增vue ui命令,提供可视化配置(有点鸡肋)
  • 移除static文件夹,新增public文件夹,并且将index移动到public中

Vue CLI使用前提

  1. 安装NodeJs
    在官方网站中直接下载安装。官网:nodejs.cn/download/

  默认情况下安装Node和NPM 查看版本
  node -v
  npm -v

  1. webpack的全局安装
    npm install webpack -g

NPM(Node Packge Manager),是NodeJS包管理和分发工具,后续中使用NPM来安装一些依赖包。
在国内使用NPM的官方镜像是很慢的,可以使用淘宝定制的cnpm命令行工具替代npm:
npm install -g cnpm --registry=https://registry.npm.taobao.org

Vue CLI的使用

  1. 安装vue脚手架
    npm install -g @vue/cli

  查看安装的版本
  vue --version

  1. 使用脚手架初始化项目
    vue create my-project

项目的名字不能包含大写字母
使用“enter”和“空格”键,来设置项目的一些配置项

Vue项目的配置修改

Vue CLI 3.X之后的版本配置文件被隐藏起来,在项目文件夹的:node_modules/@vue/cli-service/lib/webpack.config.js

别名配置

在项目中,经常导入某些文件下的接口和图片,那么使用相对路径比较麻烦。可以给文件夹设置别名,方便操作。

别名配置

如何修改配置
最简单的办法是在项目中新建:vue.config.js文件,在文件中设置一些配置项。

路由(vue-router)

前端路由的核心:改变URL,但是页面不进行整体的刷新

如何实现?

  • URL的hash
  • HTML5的history模式

URL的hash
  URL的hash也就是锚点(#),本质上是改变href属性。通过直接赋值location.hash来改变href,但是页面不发生刷新。

URL的hash

HTML5的history模式
  history接口是HTML5新增的,主要有以下几种模式该bainURL而不刷新页面

  history.pushState()

pushState

  history.replaceState()

replaceState

初识vue-router

vue-router是基于路由组件
  路由用于设定访问路径,将路径和组件映射起来,在vue-router的单页面应用中,页面的路径的改变就是组件的切换

安装和使用vue-router

  • 安装vue-router
    npm install vue-router --save

  • 使用vue-router

    第一步:导入路由对象,并且调用 Vue.use(VueRouter)

     import Vue from 'vue'
     import VueRouter from 'vue-router'
    
     Vue.use(VueRouter);
    

    第二步:创建路由实例,并传入路由映射配置

    const routes = [
    ]
    
     const router = new VueRouter({
           routes,
     })
    

    在这一步中,需要配置组件和路径映射表

    1. 创建路由组件
    2. 配置路由映射
      import Home from './components/home'
      import About from './components/about'
      
      const routes = [{
         path: '/home',
         component: Home
      },
      {
         path: '/about',
         component: About
       }
      ]
      

      注意:routes的写法不可写成 routers

    第三步:在Vue实例中关在创建的路由实例

    import router from './router'
    
    new Vue({
      render: h => h(App),
      router
    }).$mount('#app')
    

    第四步:使用路由,通过和

    import router from './router'
    
    <router-link to='/home'>home</router-link>
    <router-link to='/about'>about</router-link>
    <router-view></router-view>
    

项目的目录文件

项目的目录文件

路由的几个配置

路由的默认配置
默认情况下进入网站首页,显示首页组件,只需要在配置中配置一个映射

const routes = [{
        path: '/',
        redirect: '/home'
    },
    {
        path: '/home',
        component: Home
    }
]

在routes中配置一个映射
  path配置的是根路径: /
  redirct是重定向,也就是将根路径重定向到 /home 的路径下

HTML5的History模式
在介绍前端路由的时候,改变路径的方式有了两种

  • URL的hash
  • HTML5的history

默认情况下,路径使用的是URL的hash

如果使用HTML5的history模式,需要在创建router实例时进行配置即可

const router = new VueRouter({
    routes,
    mode: 'history'
})

router-link几个属性

在路由跳转时,使用了的属性:to,用于指定跳转的路径

还有一些其他属性:

  • tag
    指定渲染成什么组件,默认是渲染成
    用法:<router-link to='/home' tag='button'>home</router-link>
  • replace
    replace不会留下history记录,指定replace的情况下,后退键不能返回到上一页
    用法:<router-link to='/home' tag='button' replace>home</router-link>
  • active-class
    当对应的路由匹配成功时,会自动给当前元素设置一个 router-link-active的class,可以使用linkActiveClass属性来修改这个默认的class的名字 ,下面更改为: active
     const router = new VueRouter({
         routes,
         linkActiveClass: 'active',
      })
    

路由代码跳转

在业务中可能需要在执行的JavaScript代码中进行路由的跳转,使用router的push()方法

路由代码跳转

路由懒加载

路由中定义了很多不同的页面,这些页面会被打包放在一个js文件中
这些页面放在一个js中,必然造成页面非常的大
如果我们一次性从服务器请求下来这个页面,可能要花费一定的时间,甚至在用户电脑上出现短暂的空白,为避免这种情况,可以使用路由懒加载。

路由懒加载做了神马?

  1. 将路由对应的组件打包成一个个的js代码块
  2. 只有在路由被访问时,才去加载对应的组件

懒加载的方式

  1. 结合Vue的异步组件和webpack的代码分析

    const Home = resolve => {
        require.ensure(['./components/home.vue'], () => { resolve(require('./components/home.vue')) })
    };
    
  2. AMD写法
    const Home = resolve => require(['./components/home.vue'], resolve);

  3. ES6写法
    const Home = () => import ('./components/home')

    下图是使用ES6的懒加载方法,在打包dist文件夹中明显多了几个文件

    懒加载的方式

动态路由

某些情况下,一个页面的path路径可能不是确定的,可能是变量动态渲染,接下来用一个例子来说明一下

首先,新建一个user组件:user.vue ,在配置路由时,将不确定的地方使用冒号 :变量的方式

{
    path: '/user/:id',
    component: user
}

使用路由

<router-link :to="'/user/' + id">user</router-link>  

 data(){
    return{
      id: 'ID09hj'
    }
 }

在user组件获取动态路由参数

<h2>{{$route.params.id}}</h2>

嵌套路由

在页面home页面中,如果希望通过/home/new 和 /home/message 访问一些内容,就需要使用到嵌套

使用嵌套路由的的步骤:

  1. 创建相应的组件news 和 message ,使用children 属性,在路由映射表配置对应的子路由(注意子路由不带:/)
    <!--引入组件-->
     const news = () => import ('./components/news')
     const message = () => import ('./components/message')
    
    <!--配置路由-->
      {
         path: '/home',
         component: Home,
         children: [{
                 // 配置首页默认显示的组件
                 path: '',
                 redirect: 'new'
             },
             {
                 path: 'new',
                 component: news
             },
             {
                 path: 'message',
                 component: message
             }
    
         ]
    
     },
    
  2. 在home组件内使用路由
    <h1>home 组件</h1>
    
    <router-link to="/home/new">new</router-link>
    <router-link to="/home/message">message</router-link>
    <router-view></router-view>
    

路由传参

通过路由传递参数主要有两种方式:params(动态路由) 和 query

params的方式(参考动态路由)

  • 路由配置:/router/:id
  • 传递方式:属性to跟上对应的值 比如:/router/123

query的方式

  • 配置路由表
  • 传递方式:设置对象,使用query作为载体
    const profile = () => import ('./components/profile')
    <!--创建profile组,配置路由表-->
    {
      path: '/profile',
      component: profile
    }
    
    <!--1. router-link方式-->
    <router-link :to="{
        path:'/profile',
        query:{name:'why',age:18} }"
    >profile</router-link>
    
    <!--2.javascript方式-->
    methods:{
      toProfile(){
        this.$router.push({
          path:'/profile',
          query:{name:'hyb',age:23}
        })
      }
    }
    
    <!--在profile组件中获取query-->
    <p>query: {{$route.query}}</p>
    

$router 和$route 的区别
$router 是VueRouter实例,想要导航到不同的URL,则使用$router.push方法
$route 是当前router跳转的路由 可以获取name 、path 、query、params等属性值

导航守卫

在一个SPA应用中如何改变网页上的标题?

网页标题是通过<title>来显示,但是SPA只有一个固定的HTML页面,切换不同的组件,标题并不会改变。那么比较容易想的办法就是在每个路由对应的vue组件中修改,通过mounted周期函数,通过调用JavaScript的window.document.title = "标题"的方法实现。

mounted(){
    window.document.title = '用户中心';
}

但是,当页面比较多时,这种方式不容易维护(每个页面都有类似的代码)

有没有更好的办法? =》 使用导航守卫

神马是导航守卫?

  • vue-router提供的导航守卫主要用于监听路由的进入和离开
  • vue-router提供了beforeEach 和 afterEach钩子函数,他们会在路由改变前和改变后触发

使用vue-router提供了beforeEach钩子函数修改标题

  • 在路由(router.js)中定义组件对应的标题,利用meta
  • 在路由(router.js)文件中,利用导航守卫,修改标题
    <!--在路由表 利用meta 定义标题-->
    const routes = [{
          path: '/about',
          component: About,
          meta: {
              title: '关于'
          },
    
      },
      {
          path: '/user/:id',
          component: user,
          meta: {
              title: '用户中心'
          },
      }]
      
      <!--
      导航钩子的三个函数解析:
          to:即将进入的路由对象
          from:即将离开的路由对象
          next:调用该方法,进入下一个钩子函数 (必写,不然程序出问题)
      -->
      router.beforeEach((to, from, next) => {
          window.document.title = to.meta.title;
          next()
     })
    

导航守卫的分类

  1. 全局守卫 (上边使用的导航守卫)
  2. 路由独享守卫
  3. 组件守卫

后边两种守卫的用法可以参考 官网

keep-alive遇见vue-router

keep-alive 是Vue的一个组件,目的是

  • 保存组件的保留状态
  • 缓存组件,避免创建和销毁
    <!--exclude和include 可以对包含或不包含某些路由,若都包含,就不需要另外配置-->
    <keep-alive>
       <router-view></router-view>
    </keep-alive>
    

两个重要的属性:exclude和include

  • include:字符串或正则表达,只有匹配的组件会被缓存
  • exclude:字符串或正则表达,任何匹配的组件都不会被缓存

开发中的小问题
在开发中经常遇到一个问题,在一个路由里面嵌套子路由,如何在切换父路时,依然保持子路由的状态。

开发中的小问题

如上图所示,有三个父路由(home,about,user),在父路由中有两个子路由(new,message),当我们点击message路由后,就保持message页面

  1. 在home组件中使用activated,deactivated钩子函数 (不行滴)
      data(){
         return {
             path:'/home/new',
         }
     },
     
     activated(){
          console.log("--activated-");
          this.$router.push(this.path);
     
      },
     
      deactivated(){
         this.path = this.$route.path;
      },
    

这种方法不能实现功能,因为deactivated勾子函数会将点击的路由赋值给path

  1. 在home组件中使用activated,beforeRouteLeave (组件守卫的方法) (可行)
      data(){
         return {
             path:'/home/new',
         }
     },
     
     activated(){
          console.log("--activated-");
          this.$router.push(this.path);
     
      },
     
     beforeRouteLeave (to, from, next) {
         this.path = this.$route.path;
         next();
    }
    

TabBar 实现思路

效果图:

TabBar 实现思路

  1. 定义TabBar组件,在APP中使用,设置相关样式置于页面底部
  2. TabBar中显示的内容由外界决定
    • 定义插槽
    • flex布局平分TabBar
  3. 自定义TabBarItem, 可传入图片和文字
    • 定义TabBarItem,并且定义两个插槽:图片、和文字
    • 给插槽外侧包装div,用于设置样式
    • 填充插槽,实现底部TabBar效果
  4. 传入高亮图片
    • 定义另外一个插槽,用于插入高亮图片
    • 定义变量,用于控制,是否显示正常状态还是点击状态
  5. TabBarItem绑定路由数据
    • 安装路由
    • 完成router.js,并创建路由
  6. 点击item跳转到对应路由,并且动态决定是否是激活状态
    • 监听item 的点击,通过 this.$router.push替换路由路经
    • 通过 this.$route.path.indexOf(this.link) !== -1来判断是否是active
  7. 动态计算active 样式
    • 封装新的计算属性:this.isActive ? {color :red} : {}

Vuex

Vuex 是什么?
官方解释:专为Vue.js应用程序开发的状态管理模式,它采用 集成式存储管理 应用的所有组件的状态,并以响应的规则保证状态以一种可预测的方式发生变化。

用人话解释
状态管理模式,集成式存储管理这些词有点高大上,其实可以简单的看成是把需要多个组件共享的变量全部存储在一个对象里面。然后将这个对象存放在顶层的Vue实例中,让其他组件可以使用。那么多个组件就可以共享这个对象中的所有变量属性。

为啥官方还要专门出一个插件Vuex那?自己封装一个不就行了。
首先自己封装肯定可以。VueJsd带给我们最大的便利是什么?响应式。如果自己封装实现的对象如果保证它里面所有属性做到响应式还是比较麻烦的,因此Vuex横空出示。

管理什么状态?
在大型项目中,会经常遇到多个状态,在多个界面之间的共享问题

  • 比如用户的登陆状态、用户名称、头像、地理信息问题
  • 商品的收藏、购物车的物品

这些状态信息,我们都可以放在统一的地方,对它进行保存和管理,而且还是响应式的

单页面的状态管理

在单个组件中进行状态管理是一件非常简单的事情

单页面的状态管理

如图所示

  • State:状态(在这里是 data中的属性)
  • View:视图层,针对State的变化,显示不同的信息
  • Actions:这里Actions主要是用户的各种操作:点击输入等,会导致状态的变化

下面以 counter 值的变化为例来说明

<!--State 状态-->
data(){
    return {
        counter: 0,
    }
},  

<!--View 试图层-->
<div>当前计数:{{ counter }}</div>  

<!--Actions 点击-->
methods: {
    add(){
        this.counter++;

    },

    subtract(){
        this.counter--;

    }
},

多界面状态管理

在多个页面,会经常遇到两种情况

  • 多个视图都依赖同一个状态(一个状态改了,多个页面需要进行更新)
  • 不同页面的Actions都想修改同一个状态(例如:Home.vue想要修改,Profile.vue也想要修改这个状态)

全局单例模式(大管家)
我们现在要做的就是将共享的状态抽离出来,交给大管家,进行统一管理
之后,每个视图,按照规定好规定,进行访问和修改操作
这是就是Vuex背后的基本思想

下图是官方给出的处理流程

官方给出的处理流程

多页面简单的例子(计数)

  1. 安装Vuex npm install vuex --save ,创建 index.js 文件
     import Vue from 'vue'
     import Vuex from 'vuex'
    
     Vue.use(Vuex)
    
     const store = new Vuex.Store({
        state: {
          count: 2
        },
         mutations: {
             increment(state) {
                 state.count++
             },
             decrement(state) {
                 state.count--;
             }
         }
    })
    
    export default store
    
  2. 让所有Vue组件都可以使用这个store对象
    在main.js文件中,导入store对象,并且放在new Vue 中
    这样,在其他Vue组件中,我们就可以通过this.$store的方式,获取到这个store 对象
       import store from './vuex'
       new Vue({
           render: h => h(App),
           store
        }).$mount('#app')
    
  3. 在组件中,使用Vuex的count
    <div>当前计数:{{ count }}</div>
    button @click="add">+1 </button>
    <button @click="subtract">-1 </button>
        
    computed: {
        count(){
            return this.$store.state.count
        }
    },
    
    methods: {
        add(){
            this.$store.commit('increment');
            <!--this.$store.state.count++;-->
        },
    
        subtract(){
            this.$store.commit('decrement');
        }
    },
    

注意事项
我们通过提交mutation的方式,而非直接改变this.$store.state.count(在add注释部分),这是因为Vuex可以使用Vue Devtools工具明确的追踪到状态的变化

Vuex核心概念

  • State
  • Getters
  • Mutation
  • Action
  • Module

State单一状态树
将状态保存一份,统一保存到State中,不要分散保存

Getters
有时间,我们需要从store 中获取一些state变异后的状态,用法和组件的计算属性功能类似(computed)

state: {
    count: 2,
    stuedents: [
        { id: 110, name: '张三', age: 23 },
        { id: 112, name: '李四', age: 18 },
        { id: 112, name: '王五', age: 26 },
        { id: 114, name: '赵六', age: 9 },
    ]
},

getters: {
    overAgeCount: state => {
        return state.stuedents.filter(student => student.age > 20).length;
    }
},

<!--使用--> 
<h2>年龄大于20的个数:{{$store.getters.overAgeCount}}</h2>

getters的方法中不仅可以传递state参数,同样也可以传递getters参数

state: {
    count: 2,
    stuedents: [
        { id: 110, name: '张三', age: 23 },
        { id: 112, name: '李四', age: 18 },
        { id: 112, name: '王五', age: 26 },
        { id: 114, name: '赵六', age: 9 },
    ]
},

getters: {
    overAgePerson: state => {
        return state.stuedents.filter(student => student.age > 10);
    },
    overAgeCount: (state, getters) => {
        return getters.overAgePerson.length;
    }
},

<!--使用--> 
<h2>年龄大于20的个数:{{$store.getters.overAgeCount}}</h2> 

getters 默认是不能传递 额外 参数的,如果希望传递参数,那只能让getters本身返回一个参数

state: {
    count: 2,
    stuedents: [
        { id: 110, name: '张三', age: 23 },
        { id: 112, name: '李四', age: 18 },
        { id: 112, name: '王五', age: 26 },
        { id: 114, name: '赵六', age: 9 },
    ]
},

getters: {
    overCount(state) {
        // return function(age) {
        //     return state.stuedents.filter(student => student.age > age).length;
        // }
        return age => {
            return state.stuedents.filter(student => student.age > age).length;
        }
    }
},

<!--使用-->
<h2>年龄大于某个数的人的个数:{{$store.getters.overCount(20)}}</h2>

Mutation
Vuex的store状态的更新唯一方式:提交Mutation

Mutation主要有两部分组成

  • 字符串的事件类型type(Mutation的方法名)
  • 一个回调函数(handler),该回调函数的第一个参数就是state

mutation的定义方式

state: {
    count: 2
},

mutations: {
    increment(state) {
        state.count++
    }
}

在组件中通过mutation更新

<button @click="add">+1 </button>  

methods: {
    add(){
        this.$store.commit('increment');
    }
},

Mutation传递参数
在通过mutations更新数据的时候,有可能携带一些额外的参数,参数被称为是mutation的载荷(Payload)

mutation的定义方式

state: {
    count: 2
},

mutations: {
    addNmber(state, value) {
        state.count += value;
    }
}

在组件中通过mutation更新

<button @click="addNmber(5)">+5</button>
 
addNmber(value) {
    this.$store.commit('addNmber',value);
},

上边是一种Mutation提交风格,亦可以用下面的方式,但是接收到的参数是传入的对象

state: {
    count: 2
},

mutations: {
    addNmber(state, payLoad) {
        state.count += payLoad.count;
    }
}


<button @click="addNmber(5)">+5</button>

this.$store.commit({
    type: 'addNmber',
    count: value
})

如果参数不是一个的话?
如果我们有很多参数需要传递,这时候,我们通常以对象的方式传递,再从对象中抽取出响应的信息

Mutation响应规则
Vuex的store中的state是响应式的,当state中的数据发生变化时,Vue组件会自动更新
这就必须遵守Vuex对应的规则:

  • 提前在store中初始化好所需的属性
  • 当给state中的对象添加新属性时,使用下面的方式
    方式一:使用 Vue.set(obj, 'newProp', value)
    方式二:用新对象给旧对象重新赋值

修改store中初始化好所需的属性,是响应式的
vuex文件

state: {
    info: { 'name': 'hyb', age: 18 },
},

mutations: {
    changeAge(state, age) {
        state.info.age = age;
    }
}

在组件中修改age属性

<!-- info -->
<h2>{{ $store.state.info }}</h2>  
<button @click="changAge(23)">改变info的age</button>

methods: {
    changAge(age){
        this.$store.commit('changeAge', age);
    }
}

给info增加新的属性
vuex文件

state: {
    info: { 'name': 'hyb', age: 18 },
},  

mutations: {
    addGender(state, gender) {
        // 这种方式做不到响应式
        // state.info.gender = gender;  

        // 1.可以做到响应式
        // Vue.set(state.info, 'gender', gender);

        // 2.用新对象给旧对象重新赋值
        state.info = {...state.info, 'gender': gender };

        // 删除对象属性
        // delete state.info.gender;
        Vue.delete(state.info, 'name');
    },
 
}

在组件中提交mutation

<!-- info -->
<h2>{{ $store.state.info }}</h2>  
<button @click="addGender('男')">新增info的gender属性</button>

methods: {
    addGender(gender) {
        this.$store.commit('addGender', gender)
    }
},

Mutation常量类型
在mutation中,我们定义了很多事件类型(mutation中的方法名),当项目越来越大时,Vuex管理的状态越来越多,需要更新状态的情况越来越多,那么就意味着Mutation中的方法越来越多,使用者就会花费大量的经历去记住这些方法,在多个文件间切换,如果不粘贴复制的话,很可能出现写错的情况

如何解决这种情况?
常见的方案就是使用常量替代Mutation事件类型,我们可以将这些常量放在单独的文件中,方便管理让整个app事件类型一目了然

具体做法

  • 创建文件:mutation-types.js(随意命名),在其中定义常量
  • 在使用的地方引入文件,在vuex中使用时加上大括号, []

使用上边 给info增加新的属性 的例子来修改
mutation-types.js

export const AAD_GENDER = 'addGender'

vuex.js

<!--引入-->
import * as types from './mutation-types'  

mutations: {
    [types.AAD_GENDER](state, gender) {
        // 这种方式做不到响应式
        // state.info.gender = gender;  

        // 1.可以做到响应式
        // Vue.set(state.info, 'gender', gender);

        // 2.用新对象给旧对象重新赋值
        state.info = {...state.info, 'gender': gender };

        // 删除对象属性
        // delete state.info.gender;
        // Vue.delete(state.info, 'name');

    }
}

在组件中提交mutation

<!-- info -->
<h2>{{ $store.state.info }}</h2>  
<button @click="addGender('男')">新增info的gender属性</button>
        
<script>
import {AAD_GENDER} from '../../mutation-types'
export default {
    name: 'category',
    methods: {
        addGender(gender) {
            this.$store.commit(AAD_GENDER, gender)
        }
    }
}
</script>

MutAtion同步函数
通常情况下,Vuex要求我们Mutation中的方法必须是同步方法

  • 主要的原因是当我们使用devtools时,devtools可以帮助我们捕捉mutation的状态变化情况
  • 但是如果是异步,devtools将不能追踪到这个操作是什么时候完成的

SO,通常情况下,不要在mutation中进行异步操作

Action
既然在Mutation中不能进行异步操作,但是在某些情况下,我们确实希望在Vuex中进行一些异步操作,比如网络请求等等,那么就需要使用Action,代码如下:
vuex.js文件

const store = new Vuex.Store({
    state: {
        info: { 'name': 'hyb', age: 18 },
    },
    mutations: {
        addGender(state, gender) {

            // 1.可以做到响应式
            Vue.set(state.info, 'gender', gender);
        }
    },

    actions: {
        aUpdateGender(context, payload) {
            setTimeout(() => {
                context.commit('addGender', payload);

            }, 1000);

        }
    }

})

在Vue组件中进行分发

<!-- info -->
<h2>{{ $store.state.info }}</h2>  
<button @click="addGender('男')">新增info的gender属性</button>

methods: {
    addGender(gender) {
        this.$store.dispatch('aUpdateGender', gender)
    }
},

在异步请求完成后,让告诉组件信息,让组件进行下一步

  1. 通过传入函数的方式完成
    vuex.js文件

    const store = new Vuex.Store({
         state: {
             info: { 'name': 'hyb', age: 18 },
         },
         mutations: {
             addGender(state, gender) {
     
                 // 1.可以做到响应式
                 Vue.set(state.info, 'gender', gender);
             }
         },
     
         actions: {
             aUpdateGender(context, payload) {
                 setTimeout(() => {
                     context.commit('addGender', payload.gender);
                     console.log("数据请求成功");
                     payload.success();
                 }, 1000);
             }
         }
    
    })
    

    组件文件

        <!-- info -->
     <h2>{{ $store.state.info }}</h2>  
     <button @click="addGender('男')">新增info的gender属性</button>
     
     methods: {
         addGender(gender) {
             this.$store.dispatch('aUpdateGender', {
                 gender,
                 success:function(){
                     console.log("完成跟新操作")
                 }
             })
         }
     },
    
  2. 将异步操作放在Promise中,在成功或失败后,调用对应的resolve或reject
    vuex.js文件

const store = new Vuex.Store({
     state: {
         info: { 'name': 'hyb', age: 18 },
     },
     mutations: {
         addGender(state, gender) {
 
             // 1.可以做到响应式
             Vue.set(state.info, 'gender', gender);
         }
     },
     
     actions: {
         aUpdateGender(context, payload) {
             return new Promise((resolve, reject) => {
                 setTimeout(() => {
                     context.commit('addGender', payload);
                     console.log("数据请求成功");
                     resolve()
                 }, 1000);
 
             })
         }
     }
 })

在组件中使用

 <!-- info -->
 <h2>{{ $store.state.info }}</h2>  
 <button @click="addGender('男')">新增info的gender属性</button>
 
 methods: {
     addGender(gender) {
         this.$store.dispatch('aUpdateGender', gender).then(res => {
             console.log("完成更新操作");
         })
     }
 },

Module(感觉用处不太大)
Module 是模块的意思,为什么要在Vuex中使用模块?

  • Vue使用单一状态树,那么意味者很多状态都会交给Vuex来管理
  • 当应用变得非常复杂时,store对象就会变得相当臃肿
  • 为了解决这个问题,Vuex允许我们将store 分割成模块(Mudule),而每个模块都拥有自己的State、mutation、action、getters等

state, 在组件中使用state时,前面需要增加模块名

vuex.js文件

const moduleA = {
    state: {
        message: 'moduleA模块'

    }
}

const store = new Vuex.Store({
    state: {
        count: 2,
    },
    mutations: {
        addGender(state, gender) {

            // 1.可以做到响应式
            Vue.set(state.info, 'gender', gender);
        }
    },

    actions: {
        aUpdateGender(context, payload) {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    context.commit('addGender', payload);
                    console.log("数据请求成功");
                    resolve()
                }, 1000);

            })
        }
    },
    modules: {
        a: moduleA
    }

})

组件内使用

<h2>{{$store.state.a.message}}</h2>

getters,和全局的用法一样,因此名字要和全局避免,可以多传入一个值,是根state
vuex.js

const moduleA = {
    state: {
        message: 'moduleA模块'
    },
    getters: {
        messageVal(state, getters, rootState) {
            return state.message + rootState.info.name;
        }
    }
}

组件内使用

<h2>{{$store.getters.messageVal}}</h2>

其他就暂且不介绍,本人认为不太适用

axios

支持多种请求方式:

  • axios(config)
  • axios.request(config)
  • axios.get(url)
  • axios.delete()
  • axios.head()
  • axios.post()
  • axios.put()
  • axios.patch()

虽然有那么多请求方式,但是使用第一种方式,已经足够应付所有情况

安装axios
npm install axios --save

发送多个请求
有时候,我们可能要同时发送两个以上的请求,需要全部请求返回后,统一处理返回数据

  • 使用axios.all,可以放入多个请求数组
  • axios.all([])返回的结果是一个数组,使用 axios.spread 可将数组[res1, res2]展开为 res1, res2
import Axios from 'axios'  

created() {
// 发送请求  
Axios.all([Axios({url: 'http://123.207.32.32:8000/home/multidata'}),
          Axios({url: 'http://123.207.32.32:8000/home/multidata'})])
          .then(Axios.spread((res1,res2) => {
            console.log(res1);
            console.log(res2);
}))
},

全局配置
在网站开发过程中,接口的BaseURL是固定的,事实上,在开发过程中,很多参数都是固定的,因此我们可以利用axiox的全局配置对公共参数进行抽取

import Axios from 'axios'  

created() {

// 提取全局的配置
Axios.defaults.baseURL = 'http://123.207.32.32:8000'     

// 发送请求 
Axios.all([Axios({url: '/home/multidata'}),
          Axios({url: '/home/multidata'})])
          .then(Axios.spread((res1,res2) => {
            console.log(res1);
            console.log(res2);
}))

},

常见的配置选项

常见的配置选项

axios封装
为什么要进行封装?

  • 在整个项目中,全部使用Axios,如果某一天我们需要换网络请求的框架时,要在每一处都要替换,会有很大的时间成本
  • 对于一些公共的配置可以在封装的文件中统一配置
    新建一个request.js文件
    import Axios from 'axios'
    // 提取全局的配置
    Axios.defaults.baseURL = 'http://123.207.32.32:8000'
    Axios.defaults.timeout = 100
    
    export default function axios(option) {
        return Axios(option)
    }
    
    
    在组件中使用封装的对象,请求数据
    import request from './request.js'
    
    created() {
        request({url: '/home/multidata'}).then(result => {
          console.log('result:',result);
    
        }).catch(err => {
          console.log('err:',err);
        }) 
    },
    

axios实例
为什么要创建axios的实例那?
在后续开中,某些配置可能与原来的配置不太一样,比如(baseURL,timeout或content-Type等),因此我们可以创建多个实例,进行响应的配置信息 request.js文件

import Axios from 'axios'

export function axiosInstanceOne(option) {
    // 1. 创建axios实例
    const axiosInstance = Axios.create({
        baseURL: 'http://123.207.32.32:8000',
        timeout: 5000
    });

    // 2.传入对象进行网络请求
    return axiosInstance(option);
}


export function axiosInstanceTwo(option) {
    // 1. 创建axios实例
    const axiosInstance = Axios.create({
        baseURL: 'http://123.207.32.32:8000',
        timeout: 5000
    });

    // 2.传入对象进行网络请求
    return axiosInstance(option);
}

在组件中进行网络请求

import {axiosInstanceOne,axiosInstanceTwo} from './request.js'

created() {
    axiosInstanceOne({url: '/home/multidata'}).then(result => {
      console.log('result:',result);
    
    }).catch(err => {
      console.log('err:',err);
    }) 

    axiosInstanceTwo({url: '/home/multidata'}).then(result => {
      console.log('result:',result);
    
    }).catch(err => {
      console.log('err:',err);
    }) 
}

拦截器
axios提供了俩个拦截器,分别是请求拦截和响应拦截
请求拦截中经常做的事情

  • 当发送网络请求时,在页面中添加loading组件,作为加载动画
  • 某些请求要求用户必须登录,判断用户是否有token,若无跳转到login 页面
  • 对请求的参数进行序列化config.data = qs.stringify(config.data)
// 请求拦截
axiosInstance.interceptors.request.use(config => {
    console.log("请求拦截success方法");
    console.log(config);
    return config
}, err => {
    console.log("来到request拦截failure中");
    console.log(err);
    return err;
})

响应拦截中经常做的事

  • 响应成功拦截中,主要对数据过滤,拿到data数据return response.data
  • 响应失败拦截中,可以根据status判断报错的错误码,跳转到不同页面
// 响应拦截  
axiosInstance.interceptors.response.use(response => {
    console.log("响应拦截success方法");
    return response.data;

}, err => {
   
    if (err && err.response) {
        switch (err.response.status) {
            case 400:
                err.message = '请求错误'
                break
            case 401:
                err.message = '未授权的访问'
                break
        }
   
   }
   return err;

})

mall 商城实战

1.将代码放到gitHub上

  1. 将本地git仓库关联到github上 git remote add origin git@github.com:hinin/vue.js_erson_wdnotes.git
  2. 同步远程github和本地git仓库 git pull --rebase origin master
  3. 将新创建的文件添加到本地git代码仓库查看:git status,添加:git add . (git add --all)
  4. 提交add的文件到本地git仓库 git commit -m "describe"
  5. 提交到远程仓库 git push -u origin master

2.项目目录划分

项目目录划分
主要对src下的文件夹进行划分管理:

  • assets/css 公共css
  • assets/image 图片
  • assets 静态资源
  • common 公共js
  • components/commom 和业务不相关的组件
  • components/content 和业务相关组件
  • network 网络请求
  • router 前端路由
  • store vuex
  • views 不同模块组件

3.css引入

  • normalize.css
  • base.css
    :root 定义css变量

4. 别名配置

在项目中多次引入目录时,目录层次太多,书写的时候不太方便,可以给一些常用的文件夹配置别名
在根目录下,创建vue.config.js(这个名字是固定的,不能改)

module.exports = {
    configureWebpack: {
        resolve: {
            alias: {
                'assets': '@/assets'
            }
        }
    }
}

5. tabbar的封装

巧妙之处利用 route

6. 导航栏navbar的封装

  • 三个slot
  • flex布局 左右固定 中间自适应

7. 二次封装网络请求axios

  • axios实例的创建
  • 二次封装