Vue组件化编程

前言

上一章我们讲解了Vue的核心功能,也就是核心库;我们是在每一个页面中都去引入vue.js文件;然后通过new Vue()来进行View和Model的管理;但是目前为止,我们还存在一个问题,比如A页面需要定义某个功能,这个功能肯定涉及到data的数据,或者一些methods的行为;那么如果我们在B页面也需要相同的功能呢?难道我们要重新定义一份吗?显然肯定不行。那么组件的必要性就出来了;

那么从上述的理解来说:这个公用的功能可能要包含三部分:HTML代码、CSS代码、vm实例;

一、vue 组件

组件的出现,就是为了拆分Vue实例的代码量的,能够让我们以不同的组件,来划分不能的功能模块,将来需要什么样的功能,就可以去调用。

组件和模块化的不同:

​ 模块化:是从代码逻辑的角度进行划分的,方便代码分层开发,保证每个功能模块职能单一

​ 组件化:是从UI界面的角度进行划分的,前端的组件化,方便UI组件的重用

全局组件定义的方式

使用Vue.extend

Vue.extend 属于 Vue 的全局 API,在实际业务开发中我们很少使用,因为相比常用的 Vue.component 写法使用 extend 步骤要更加繁琐一些.

<div id="root">
    <login></login>
</div>

<script type="text/javascript">
    // 方法1:使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
    // 创建构造器
    var login = Vue.extend({
        //定义组件的模版
        template: "<h1>欢迎使用登录模块</h1>"
    })
    
    var vm = new Vue({
        el: "#root"
    })
    
    //需要通过 new login().$mount('#mount-point') 来挂载到指定的元素上。
    new login().$mount('#root')
</script>

使用Vue.component

在注册一个组件的时候,我们始终需要给它一个名字。比如在全局注册的时候我们已经看到了

Vue.component('my-component-name', { /* ... */ })

该组件名就是 Vue.component 的第一个参数。

组件名大小写

定义组件名的方式有两种:

使用 kebab-case

Vue.component('my-component-name', { /* ... */ })

当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 <my-component-name>

使用 PascalCase

Vue.component('MyComponentName', { /* ... */ })

当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说 <my-component-name><MyComponentName> 都是可接受的。注意,尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。

<div id="root">
    <register></register>
</div>

<script type="text/javascript">
    // 方法2:直接使用Vue.component方法
    /*
            注意,使用Vue.component 定义全局组件的时候,组件名称使用了驼峰命名,则在引用
            组件的时候,需要把大写的驼峰改为小写字母,同时两个单词之间使用“-”进行连接,
            如果没有小驼峰命名,直接使用即可
            */
    Vue.component("register", {
        //注意组件里只能有一个根元素,下面的方式是错误的
        //	template:"<h1>欢迎新用户注册</h1><h3>注册第一步</h3>"
        //可以使用一个div将所有元素包括住
        template: "<div><h1>欢迎新用户注册</h1><h3>注册第一步</h3></div>"
    })
    var vm = new Vue({
        el: "#root"
    })
</script>

使用template标签
<div id="root">
    <main-nav></main-nav>
</div>
<!-- 在被控制的app标签外面,使用tempalte元素,定义组件的HTML模板结构 -->
<template id="nav_tpl">
    <div>
        <ul>
            <li><a href="#">首页</a></li>
            <li><a href="#">新闻</a></li>
            <li><a href="#">产品</a></li>
        </ul>
    </div>
</template>
<script type="text/javascript">
    // 方法3,在被控制的#root外部定义组件结构,这种方式有代码高亮
    // 组件命名同样需要注意小驼峰命名的问题
    Vue.component("main-nav", {
        template: "#nav_tpl"
    })
    var vm = new Vue({
        el: "#root"
    })
</script>

定义局部组件

<div id="root">
    <page-header></page-header>
    <page-footer></page-footer>
</div>

<script type="text/javascript">
    // 方法3,在被控制的#root外部定义组件结构,这种方式有代码高亮
    // 组件命名同样需要注意小驼峰命名的问题
    var vm = new Vue({
        el: "#root",
        components: {
            pageHeader: {
                template: '<h2>我是网页头部内容</h2>'
            },
            pageFooter: {
                template: '<h2>&copy;版权所有</h2>'
            }
        }
    })
</script>

全局组件和局部组件使用区别

全局组件注册以后,在不同的Vue实例挂载点内都可以使用,而局部的只能在注册的Vue实例对应的挂载点里使用

组件中的data

  • 组件可以有自己的data数据
  • 组件的data和实例的data有点不一样,实例中的data可以为一个对象,也可以是一个方法,但是组件的data必须是一个方法
  • 组件中的data除了必须为一个方法以外,这个方法内部,还必须返回一个对象才行
  • 组件中的data数据使用方式和实例中的data使用方式完全一样
<div id="root">
    <main-nav></main-nav>
</div>
<template id="nav_tpl">
    <div>
        <nav>{{user}}的个人主页</nav>
        <ul>
            <li>姓名:{{user}}</li>
            <li>年龄:{{age}}</li>
            <li>地址:{{address}}</li>
        </ul>
    </div>
</template>
<script type="text/javascript">
    /*
		组件可以有自己的data数据
		组件的data和实例的data有点不一样,实例中的data可以为一个对象,但是组件的data必须是一个方法
		组件中的data除了必须为一个方法以外,这个方法内部,还必须返回一个对象才行
		组件中的data数据使用方式和实例中的data使用方式完全一样
		*/
    Vue.component("main-nav", {
        template: "#nav_tpl",
        // data:{
        // 	user:"张三"
        // }
        data() {
            return {
                user: "张三",
                age: 12,
                address: "河南省郑州市"
            }
        }
    })
    var vm = new Vue({
        el: "#root"
    })
</script>

创建一个可以点击的按钮,每次点击按钮,按钮后边的 数字都会加1,使用组件实现

<div id="root">
    <data-test></data-test>
</div>
<template id="test">
    <div>
        <button @click="add()">递增按钮</button><span>{{count}}</span>
    </div>
</template>
<script type="text/javascript">
    Vue.component("data-test", {
        template: "#test",
        data: function () {
            return {
                count: 1
            }
        },
        methods: {
            add() {
                this.count++;
            }
        }
    })
    var vm = new Vue({
        el: "#root"
    })
</script>

切换组件

有如下登录和注册的模板,我们来进行切换

<div id="root">
    <a href="">登录</a>
    <a href="">注册</a>
    <login></login>
    <register></register>
</div>

<template id="login">
    <div>
        <h2>用户登录</h2>
        <p>用户名:<input type="text"></p>
        <p>密码:<input type="text"></p>
    </div>
</template>

<template id="register">
    <div>
        <h2>用户注册</h2>
        <p>手机号:<input type="text"></p>
        <p>身份证号:<input type="text"></p>
    </div>
</template>
<script type="text/javascript">
    Vue.component("login", {
        template: "#login"
    })
    Vue.component("register", {
        template: "#register"
    })
    var vm = new Vue({
        el: "#root"
    })
</script>

默认效果如下:
在这里插入图片描述

实现切换后的效果如下:

在这里插入图片描述

使用v-if和v-else切换

<div id="root">
    <a href="javascript:void(0)" @click="flag = true">登录</a>
    <a href="javascript:void(0)" @click="flag = false">注册</a>
    <login v-if="flag"></login>
    <register v-else></register>
</div>

<template id="login">
    <div>
        <h2>用户登录</h2>
        <p>用户名:<input type="text"></p>
        <p>密码:<input type="text"></p>
    </div>
</template>

<template id="register">
    <div>
        <h2>用户注册</h2>
        <p>手机号:<input type="text"></p>
        <p>身份证号:<input type="text"></p>
    </div>
</template>
<script type="text/javascript">
    Vue.component("login", {
        template: "#login"
    })
    Vue.component("register", {
        template: "#register"
    })
    var vm = new Vue({
        el: "#root",
        data: {
            flag: true
        }
    })
</script>

component元素实现组件切换

component元素是一个占位符, :is 属性用来指定要展示的组件名称

<component :is="componentId"></component>

需要注意的是,:is属性的值是组件名称字符串,需要按照如下的代码,才能将组件正常展示

<div id="root">	
    <a href="javascript:void(0)"  @click="flag=true">登录</a>
    <a href="" @click.prevent="flag=false">注册</a>
    <!-- component元素是一个占位符, :is 属性用来指定要展示的组件名称 -->
    <component :is="'login'"></component>
    <component :is="'register'"></component>
</div>

所以,如果使用<component>元素就要定一个变量

<div id="root">
    <a href="javascript:void(0)" @click="componentName='login'">登录</a>
    <a href="" @click.prevent="componentName='register'">注册</a>
    <!-- component元素是一个占位符, :is 属性用来指定要展示的组件名称 -->
    <component :is="componentName"></component>
</div>

<template id="login">
    <div>
        <h2>用户登录</h2>
        <p>用户名:<input type="text"></p>
        <p>密码:<input type="text"></p>
    </div>
</template>

<template id="register">
    <div>
        <h2>用户注册</h2>
        <p>手机号:<input type="text"></p>
        <p>身份证号:<input type="text"></p>
    </div>
</template>
<script type="text/javascript">
    Vue.component("login", {
        template: "#login"
    })
    Vue.component("register", {
        template: "#register"
    })
    var vm = new Vue({
        el: "#root",
        data: {
            componentName: "login"
        }
    })
</script>

组件过渡切换

官网地址:https://cn.vuejs.org/v2/guide/transitions.html#多个组件的过渡

根据官网代码,改造案例:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport"
        content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
    <title></title>
    <!-- 引入vue.js -->
    <script src="../js/vue.js"></script>
    <style>
        .component-fade-enter-active,
        .component-fade-leave-active {
            transition: all .3s ease;
        }

        .component-fade-enter,
        .component-fade-leave-to {
            opacity: 0;
            transform: translateX(150px)
        }
    </style>
</head>

<body>
    <div id="root">
        <a href="javascript:void(0)" @click="componentName='login'">登录</a>
        <a href="" @click.prevent="componentName='register'">注册</a>
        <!-- 添加 mode="out-in",意思是让过渡效果显示为一个消失了,另一个再进来 -->
        <!-- 添加name属性,是为了设置过渡样式 -->
        <transition name="component-fade" mode="out-in">
            <component :is="componentName"></component>
        </transition>
    </div>
    <template id="login">
        <div>
            <h2>用户登录</h2>
            <p>用户名:<input type="text"></p>
            <p>密码:<input type="text"></p>
        </div>
    </template>

    <template id="register">
        <div>
            <h2>用户注册</h2>
            <p>手机号:<input type="text"></p>
            <p>身份证号:<input type="text"></p>
        </div>
    </template>
    <script type="text/javascript">
        Vue.component("login", {
            template: "#login"
        })
        Vue.component("register", {
            template: "#register"
        })
        var vm = new Vue({
            el: "#root",
            data: {
                componentName: "login"
            }
        })
    </script>
</body>

</html>

组件传值

组件传值的遇到的问题

局部定义组件的方式,需要考虑Vue实例和组件之间的传值的问题,而子组件默认无法访问到父组件的数据和methods里方法。

<div id="root">
    <product-list></product-list>
</div>
<script type="text/javascript">
    var vm = new Vue({
        el: "#root",
        data: {
            msg: "Vue实例相当于内部组件的父组件"
        },
        methods: {},
        components: {
            productList: {
                template: '<h2>{{msg}}</h2>'
            }
        }
    })
</script>

浏览器里的显示效果如下:
在这里插入图片描述

父组件向子组件传值

通过属性绑定的形式,把需要传递给子组件的数据,以属性绑定的形式,传递到子组件内部,供子组件使用。

一般情况下不管是组件还是实例,自己的数据只能自己使用,如果子组件想使用父组件的数据,我们可以通过props来接收自定义属性的方式来将父组件的数据传递给子组件;

具体流程如下:

1.在组件标签上使用自定义属性

2.在组件内部通过props上来接收自定义属性

3.接收完毕以后,就可以使用父组件的数据
​ 问题:

a. 如果需要向非子后代传递数据必须多层逐层传递

b. 兄弟组件间也不能直接 props 通信, 必须借助父组件才可以

<div id="root">
    <!-- 通过属性绑定方式传值, v-bind:自定义属性名=“传递的值” -->
    <!-- msg在root挂载点里面调用了,那么肯定可以拿到值; 我们把值赋予给product-list组件的pmsg属性 -->

    <!-- 父组件向子组件传递值,不仅可以传递data里面的数据,还可以传递methods里面的方法 -->
    <product-list :pmsg="msg" :pmethod1="method1"></product-list>
</div>
<script type="text/javascript">
    var vm = new Vue({
        el: "#root",
        data: {
            msg: "父组件的Msg"
        },
        methods: {
            method1() {
                alert("父组件定义的方法");
            }
        },
        components: {
            // 局部组件productList
            productList: {
                // 子组件的数据,都是使用自己的;如果自己没有定义,那么报错
                template: '<h2 @click="pmethod1">{{msg}} - {{pmsg}}</h2>',
                // 如果自己定义了msg,那么就不报错;
                data() {
                    return {
                        msg: "子组件自己的Msg"
                    }
                },
                // props是用来接收父组件传递进来的数据
                // props里面的内容,必须是绑定的属性名;那么props里面的数据可以当成data来用
                props: ['pmsg', 'pmethod1']
            }
        }
    })
</script>

官网摘抄:

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

<div id="root">
    <h1 @click="changeVal">父组件原始的数据:{{msg}}</h1>
    <!-- 父组件向子组件传值,不仅可以传递data里面的数据,还可以传递methods里面的方法 -->
    <!-- 
父组件向子组件传值,父数据和子数据之间形成了一个单向下行绑定
改变父组件传递的数据会影响子组件
但是改变子组件传递而来的数据,不会影响父组件数据(也不推荐改)
-->
    <product-list :pmsg="msg" :pmethod1="method1"></product-list>
</div>
<script type="text/javascript">
    var vm = new Vue({
        el: "#root",
        data: {
            msg: "父组件的Msg"
        },
        methods: {
            method1() {
                alert("父组件定义的方法");
            },
            changeVal() {
                // 改变父组件的msg,会影响子
                this.msg = "父组件更新后的Msg";
            }
        },
        components: {
            // 局部组件productList
            productList: {
                // 子组件的数据,都是使用自己的;如果自己没有定义,那么报错
                template: '<h2 @click="changeVal">父传递过来的数据pmsg: {{pmsg}}</h2>',
                // 如果自己定义了msg,那么就不报错;
                /* data() {
                        return {
                            msg: "子组件自己的Msg"
                        }
                    }, */
                methods: {
                    changeVal() {
                        // 改变传递过来的pmsg,不会影响父;而且不建议改
                        this.pmsg = "子组件更新了父组件传递过来的数据"
                    }
                },
                // props是用来接收父组件传递进来的数据
                // props里面的内容,必须是绑定的属性名;那么props里面的数据可以当成data来用
                props: ['pmsg', 'pmethod1']
            }
        }
    })
</script>

子组件向父组件传值

和父子通信不同的是,子父通信是通过子组件使用$emit去触发父组件的内置事件

具体流程如下:

1.在组件标签上使用自定义事件

2.自定义事件的事件函数是在父组件里面定义的

3.在子组件的method里面,触发这个事件 this.$emit(自定义事件名,传递的值)

问题: 隔代组件或兄弟组件间通信此种方式不合适

通过props实现

注意:取巧方式 - 其实属于父子通信

<div id="root">
    <h1 @click="updateName('小黑')">员工的原始姓名是: {{name}}</h1>
    <!-- 
父组件存储的是子组件需要的数据
比如员工的增删改查,我们就可以定义四个组件 Add  Update Delete Query
这四个组件都需要使用员工数据,那么数据就放在父组件
比如update子组件,那么父组件应该把要更新的用户数据传递给子组件;子组件应该把更新后的数据给父组件,来更新数据
-->
    <!-- 把子组件要更改的数据传递给父组件 -->
    <product-list :name="name" :show="show"></product-list>
</div>

<template id="product-list">
    <div>
        <p>你要修改的姓名是: {{name}}</p>
        <p><input type="text" placeholder="请输入新的姓名" v-model="newName"></p>
        <button @click="update">更新</button>
    </div>
</template>
<script type="text/javascript">
    var vm = new Vue({
        el: "#root",
        data: {
            name: "张三"
        },
        methods: {
            show(newName) {
                this.name = newName;
            }
        },
        components: {
            // 局部组件productList
            productList: {
                // 子组件的数据,都是使用自己的;如果自己没有定义,那么报错
                template: '#product-list',
                data() {
                    return {
                        newName: ""
                    }
                },
                methods: {
                    update() {
                        this.show(this.newName);
                    }
                },
                /* props: {
                        name: {
                            type: String
                        },
                        updateName: {
                            type: Function
                        }
                    } */
                props: ['name', 'show']
            }
        }
    })
</script>
通过自定义事件实现
<div id="root">
    <h1 @click="updateName('小黑')">员工的原始姓名是: {{name}}</h1>
    <!-- 
父组件存储的是子组件需要的数据
比如员工的增删改查,我们就可以定义四个组件 Add  Update Delete Query
这四个组件都需要使用员工数据,那么数据就放在父组件
比如update子组件,那么父组件应该把要更新的用户数据传递给子组件;子组件应该把更新后的数据给父组件,来更新数据
-->
    <!-- 把子组件要更改的数据传递给父组件 -->
    <!-- 
@show是给子组件定义一个自定义事件; 这个自定义事件绑定的事件函数是父里面的show()
什么时候触发这个事件? 当我们点击更新按钮的时候,触发这个事件即可
-->
    <product-list :name="name" @show="show"></product-list>
</div>

<template id="product-list">
    <div>
        <p>你要修改的姓名是: {{name}}</p>
        <p><input type="text" placeholder="请输入新的姓名" v-model="newName"></p>
        <button @click="update">更新</button>
    </div>
</template>
<script type="text/javascript">
    var vm = new Vue({
        el: "#root",
        data: {
            name: "张三"
        },
        methods: {
            show(newName) {
                this.name = newName;
            }
        },
        components: {
            // 局部组件productList
            productList: {
                // 子组件的数据,都是使用自己的;如果自己没有定义,那么报错
                template: '#product-list',
                data() {
                    return {
                        newName: ""
                    }
                },
                methods: {
                    update() {
                        // 触发组件绑定的事件   this.$emit()
                        this.$emit("show", this.newName)
                    }
                },

                props: ['name']
            }
        }
    })
</script>

模版系统中使用组件

如果你没有通过 import/require 使用一个模块系统,也许可以暂且跳过这个章节。如果你使用了,那么我们会为你提供一些特殊的使用说明和注意事项。

如果你还在阅读,说明你使用了诸如 Babel 和 webpack 的模块系统。在这些情况下,我们推荐创建一个 components 目录,并将每个组件放置在其各自的文件中。

然后你需要在局部注册之前导入每个你想使用的组件。例如,在一个假设的 ComponentB.jsComponentB.vue 文件中:

import ComponentA from './ComponentA'
import ComponentC from './ComponentC'

export default {
    components: {
        ComponentA,
        ComponentC
    },
    // ...
}

现在 ComponentAComponentC 都可以在 ComponentB 的模板中使用了。

二、使用 vue-cli 创建模板项目

vue-cli 概述

介绍

​ vue-cli 是一个官方发布 vue.js 项目脚手架,使用 vue-cli 可以快速创建 vue 项目。

​ vue-cli是一个项目脚手架工具,它支持通过模板来生成项目结构。执行vue init命令可以指定模板的名字,默认情况下vue-cli会根据传入的模块名字去github中查找模板。

​ vue-cli的模板分为官方模板、自定义模板和本地模板。

​ 文档地址:https://cli.vuejs.org/zh/

​ github地址:https://github.com/vuejs/vue-cli

​ 老版API:https://github.com/vuejs/vue-cli/tree/v2#vue-cli–

​ 新版API:https://cli.vuejs.org/zh/guide/

特点

功能丰富:
	对 Babel、TypeScript、ESLint、PostCSS、PWA、单元测试和 End-to-end 测试提供开箱即用的支持。

易于扩展:
	它的插件系统可以让社区根据常见需求构建和共享可复用的解决方案。

无需 Eject:
	Vue CLI 完全是可配置的,无需 eject。这样你的项目就可以长期保持更新了。

CLI 之上的图形化界面:
	通过配套的图形化界面创建、开发和管理你的项目

即刻创建原型:
	用单个 Vue 文件即刻实践新的灵感。

面向未来:
	为现代浏览器轻松产出原生的 ES2015 代码,或将你的 Vue 组件构建为原生的 Web Components 组件。

vue-cli 创建 vue 项目

关于旧版本

Vue CLI 的包名称由 vue-cli 改成了 @vue/cli。 如果你已经全局安装了旧版本的 vue-cli (1.x 或 2.x),你需要先通过 npm uninstall vue-cli -gyarn global remove vue-cli 卸载它。

拉取 3.x 模版 (新版本)

vue-cli 安装

先决条件:Node.js > v8.9 或更高版本 (推荐 v10 以上),npm 3+版本和Git。

# 安装方式
npm install -g @vue/cli
# OR
yarn global add @vue/cli

在这里插入图片描述

安装之后,你就可以在命令行中访问 vue 命令。你可以通过简单运行 vue,看看是否展示出了一份所有可用命令的帮助信息,来验证它是否安装成功。

你还可以用这个命令来检查其版本是否正确:

vue --version
升级

如需升级全局的 Vue CLI 包,请运行:

npm update -g @vue/cli

# 或者
yarn global upgrade --latest @vue/cli
项目依赖

上面列出来的命令是用于升级全局的 Vue CLI。如需升级项目中的 Vue CLI 相关模块(以 @vue/cli-plugin-vue-cli-plugin- 开头),请在项目目录下运行 vue upgrade

用法: upgrade [options] [plugin-name]

(试用)升级 Vue CLI 服务及插件

选项:
  -t, --to <version>    升级 <plugin-name> 到指定的版本
  -f, --from <version>  跳过本地版本检测,默认插件是从此处指定的版本升级上来
  -r, --registry <url>  使用指定的 registry 地址安装依赖
  --all                 升级所有的插件
  --next                检查插件新版本时,包括 alpha/beta/rc 版本在内
  -h, --help            输出帮助内容
卸载vue-cli
npm uninstall @vue/cli -g
创建项目

在命令行下输入 vue create 项目名

# 创建指定名称的项目
vue create vue_demo

注意,vue create只有3.0以上的版本才可以使用

输入完命令后,如下图,它会让你去选择一个预设:

​ 第一个(vue2)和第二个(vue3)是默认的,选了bable,eslint

​ 第三个是手动选择,这里我是选了手动选择

在这里插入图片描述

选了手动选择,就会给你更多特性供你选择,如下图

​ 上下键可以切换,这里我多选一个CSS,然后回车即可

在这里插入图片描述

选择vuejs的版本

在这里插入图片描述

选择css预处理器,根据需求选择

在这里插入图片描述

你选择ESLint规则,这里我选的ESLint + Airbnb config

在这里插入图片描述

保存文件时lint 还是提交的时候 ,这里我选的保存文件

在这里插入图片描述

这里问你bable,postCss,ESLint,这些是放在专用配置文件中,还是放在package.json里,选第一项

在这里插入图片描述

然后会问你上面的那些预设是否要保存,这里我选的不保存

​ 保存:下次创建项目还是用这个预设

​ 不保存:如果选择下次还要重新设置一遍

在这里插入图片描述

自动添加依赖

在这里插入图片描述

安装完毕

在这里插入图片描述

进入目录,开始编译 (就是图里那两行蓝色的命令)

在这里插入图片描述

编译成功啦

在这里插入图片描述

浏览器访问测试

在这里插入图片描述

拉取 2.x 模板 (旧版本)

Vue CLI >= 3 和旧版使用了相同的 vue 命令,所以 Vue CLI 2 (vue-cli) 被覆盖了。如果你仍然需要使用旧版本的 vue init 功能,你可以全局安装一个桥接工具:

vue-cli 安装
npm install -g @vue/cli-init  
# 或 (推荐)
cnpm install -g vue-cli

在这里插入图片描述

vue-cli 卸载
npm uninstall vue-cli -g
创建项目
# 语法
vue init <模板名称>  <项目名称>

# 示例
vue init webpack vue-demo

上面的命令从vuejs-templates / webpack提取模板,提示您输入一些信息,并在生成项目./vue-demo/

在这里插入图片描述

创建项解析
  1. Project name:项目名称
  2. Project description:项目描述
  3. Author:作者
  4. Vue build:vue的构建方式。第一种 Runtime + Compiler 运行加编译(推荐),第二种 Runtime-only 仅运行
  5. Install vue-router? :是否安装 vue-router ,这是官方的路由,大多数情况下都使用,输入“y”后回车即可。
  6. Use ESLint to lint your code?:是否使用 ESLint 管理代码,ESLint 是个代码风格管理工具,是用来统一代码风格的,一般项目中都会使用。
  7. Pick an ESLint preset:选择一个 ESLint 预设,编写 vue 项目时的代码风格,选择 Standard (标准)即可。我们也可以选择Airbnb
  8. Setup unit tests:是否安装单元测试,选择安装即可。
  9. Pick a test runner:选择一个单元测试运行器,选择 Jest 即可。
  10. Setup e2e tests with Nightwatch? :是否安装 e2e 测试框架 NightWatch ,(e2e,也就是 End To End,就是所谓的“用户真实场景”)。
  11. Should we run npm install for you after the project has been created?:项目创建后是否要为你运行“ npm install ”。
项目模版分类

官方模板

在使用前可以先用vue list 命令查询有哪些模板可以使用,然后通过vue init命令来生成相应的项目结构。模板分为基础和高级两个版本,其中基础版本用于快速构建原型;高级版本用于正式开发。所有模板都支持*.vue组件。

所有官方项目模板都在vuejs-templates组织中。将新模板添加到组织后,您将可以运行vue init <template-name> <project-name>以使用该模板。您还可以运行vue list以查看所有可用的官方模板。

当前可用的模板包括:

  • webpack-具有热重载,整理,测试和CSS提取功能的全功能Webpack + vue-loader设置。
  • webpack-simple-拥有基础功能的Webpack+vue-loader用于快速开发。
  • browserify -拥有高级功能的Browserify+vueify用于正式开发。
  • browserify-simple-拥有基础功能的Browserify+vueify用于快速开发。
  • pwa-基于webpack模板的vue-cli的PWA模板
  • simple-单个HTML,用于开发最简单的Vue.js应用。

自定义模板

当官方模板不能满足需求时,我们可以fork官方模板按照自己的需求修改后,通过vue-cli命令生成基于自己模板的项目结构:

vue init username/repo my-project

本地模板

除了从github下载模板外,还可以从本地加载模板:

vue init ~/fs/path/to-custom-template my-project
安装及运行
# 进入目录
cd vue-demo

# 安装依赖(比较慢)
npm install

# 运行
npm run dev

# 访问
http://localhost:8080/

在这里插入图片描述

注:在config/index.js中可修改默认端口号和自动打开浏览器的;

在这里插入图片描述

vue-cli 项目目录结构

3.x以上版本

  • dist:用于存放使用 npm run build 命令打包的项目文件
  • node_modules:用于存放我们项目的各种依赖
  • public:用于存放静态资源
    • public/index.html:是一个模板文件,作用是生成项目的入口文件。浏览器访问项目的时候就会默认打开的是生成好的index.html。
    • favicon.ico 图标文件,可替换
  • src:是存放各种vue文件的地方。
    • src/assets:用于存放着各种静态文件,比如图片。
    • src/components:用于存放我们的公共组件,比如header、footer等。
    • src/router/index.js:vue-router路由文件。需要引入src/views文件夹下的.vue,配置path、name、component。
    • src/store/index.js:是vuex的文件,主要用于项目里边的一些状态保存。比如state、mutations、actions、getters、modules。
    • src/views:用于存放我们写好的各种页面,比如Login.vue,Home.vue。
    • src/App.vue:是主vue模块,主要是使用router-link引入其他模块,App.vue是项目的主组件,所有的页面都是在App.vue下切换的。
    • src/main.js:入口文件,主要作用是初始化vue实例,同时可以在此文件中引用某些组件库或者全局挂载一些变量。
  • .gitignore:配置git上传想要忽略的文件格式。
  • babel.config.js:是一个工具链,主要用于在当前和较旧的浏览器或环境中将ES6的代码转换向后兼容(低版本ES)。
  • package.json:模块基本信息项目开发所需要的模块,版本,项目名称。
  • package-lock.json:是在npm install时候生成的一份文件,用于记录当前状态下实际安装的各个npm package的具体来源和版本号

2.x版本

  • build 目录:构建脚本目录,webpack的初始化配置
    • build.js :生产环境构建代码
    • check-version.js :检查node、npm等版本
    • utils.js :构建工具相关
    • vue-loader.conf.js :webpack loader配置
    • webpack.base.conf.js :webpack基础配置
    • webpack.dev.conf.js :webpack开发环境配置,构建开发本地服务器
    • webpack.prod.conf.js :webpack生产环境配置
  • config 目录:构建配置目录,项目初始化的配置,包括端口号等
    • dev.env.js :开发环境变量
    • index.js :项目一些配置变量
    • prod.env.js :生产环境变量
    • test.env.js :测试脚本的配置
  • node_modules 目录:存放 npm install 安装的项目依赖的模块
  • src 目录:源码目录,这里是我们要开发的目录,基本上要做的事情都在这个目录里
    • assets 目录:资源目录,放置图片,如logo等
    • components 目录:组件目录,放置组件文件
    • router 目录:路由目录,放置路由的配置
    • app.vue :页面的入口 vue 文件/根组件
    • main.js :项目的入口 js 文件
  • static 目录:放置静态资源目录,如图片、字体等。
    • .gitkeep git特点:如果文件夹没有内容,则自动忽略;但是有.gitkeep就会保留下来进行版本控制
  • test 目录:初始测试目录,可删除
  • .babelrc :ES6 语法编译配置
  • .editorconfig :定义代码格式
  • .eslintignore :eslint 检测代码忽略的文件(夹)
  • .eslintrc.js :配置 eslint 的 plugins,extends,rules
  • .gitignore :用来过滤一些版本控制的文件,比如 node_modules 文件夹
  • .postcsssrc :postcss 配置文件
  • index.html :访问的页面
  • package.json :项目配置文件,记载着项目的描述信息和依赖等
  • README.md :项目的说明文档,markdown 格式

vue-cli 构建的项目的执行顺序

这里必须要明确一点,在vue里面,一切皆组件!!!一切皆组件!!!一切皆组件!!!,特地强调一下这一点,vue的开发也是组件化的开发,这跟传统的html页面完全不一样,还有就是基本开发都是在src文件夹下面的,所以src是核心。

1、package.json

在执行npm run dev的时候,会在当前目录中寻找 package.json 文件, 有点类似 Maven 的 pom.xml 文件,包含项目的名称版本、项目依赖等相关信息。

2、webpack.dev.conf.js

从下图中可以看到, 启动 npm run dev 命令后,会加载 build/webpack.dev.conf.js 配置并启动 webpack-dev-server 。
在这里插入图片描述

3、config/*.js

webpack.dev.conf.js 中引入了很多模块的内容,其中就包括 config 目录下服务器环境的配置文件。

在这里插入图片描述

4、config/index.js

可以看到,在 index.js 文件中包含服务器 host 和 port 以及入口文件的相关配置,默认启动端口是8080,这里可以进行修改。

在这里插入图片描述

在这里插入图片描述

5、index.html

index.html 的内容很简单,主要是提供一个 div 给 vue 挂载。

在这里插入图片描述

6、main.js

main.js 中, 引入了 vue、App 和 router 模块, 创建了一个 Vue 对象,并把 App.vue 模板的内容挂载到 index.html 的 id 为 app 的 div 标签下, 并绑定了一个路由配置。

在这里插入图片描述

7、App.vue

上面 main.js 把 App.vue 模板的内容,放置在了 index.html 的 div 标签下面。查看 App.vue 的内容我们看到,这个页面的内容由一个 logo 和一个待放置内容的 router-view,router-view 的内容将由 router 配置决定。

在这里插入图片描述

8、index.js

查看 route 目录下的 index.js,我们发现这里配置了一个路由, 在访问路径 / 的时候, 会把 HelloWorld 模板的内容放置到上面的 router-view中。

在这里插入图片描述

9、HelloWorld.vue

HelloWorld 中主要是一些 Vue 介绍显示内容。

在这里插入图片描述

10、页面组成

所以,页面关系组成是 index.html 包含 App.vue,App.vue 包含 HelloWorld.vue 。

三、基于 vue-cli 编写项目

删除src目录下的components、router、App.vue、main.js;我们自己来编写这套程序;暂且不使用路由;

编写HelloWorld组件

在components目录下面新建HelloWorld.vue文件,一个.vue文件就是一个组件;

<template>
  <div>
    <h1 class="title">{{ msg }}</h1>
  </div>
</template>

<script>
// 使用默认暴露,暴露出去一个对象,这个就是我们的组件对象
export default {
  // 组件对象(与new Vue()里面传递的配置对象写法一致)
  data() {
    return {
      msg: "Hello Vue Component",
    };
  },
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.title {
  color: red;
  background: yellow;
}
</style>

编写APP根组件

<template>
  <div id="app">
    <img src="./assets/logo.png" />
    <!-- 3、使用组件标签 -->
    <HelloWorld />
  </div>
</template>

<script>
// 1、引入组件
import HelloWorld from "./components/HelloWorld";

export default {
  // 2、映射组件标签
  components: {
    HelloWorld,
  },
};
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

编写main.js

/* 入口js文件,需要在这里面创建vue实例 */

// 引入vue,注意大小写
import Vue from 'vue'
// 引入根组件
import App from './App'


/* eslint-disable no-new */
new Vue({
    // 挂载在index.html的#app元素上
    el: '#app',
    components: {
        App
    },
    // 使用组件标签,使用template替代#app里面的内容
    template: '<App/>'
})

四、项目的打包与发布

项目打包

npm run build

在这里插入图片描述

使用静态服务器工具包

# 安装
npm install -g serve

在这里插入图片描述

# 启动
serve dist

在这里插入图片描述

使用动态 web 服务器(tomcat)

修改配置: webpack.prod.conf.js

output: { 
    publicPath: '/xxx/' //打包文件夹的名称 
}

重新打包

npm run build

修改 dist 文件夹为项目名称: xxx

将 xxx 拷贝到运行的 tomcat 的 webapps 目录下

访问: http://localhost:8080/xxx

五、基于Vue-cli 使用eslint

在我们开发环境运行或打包的时候,都会报很多错误,这是因为我们集成了ESLint;

在这里插入图片描述

注:我们可以修改.eslintrc.js来进行相关配置;

说明

  1. ESLint 是一个代码规范检查工具

  2. 它定义了很多特定的规则, 一旦你的代码违背了某一规则, eslint会作出非常有用的提示

  3. 官网: http://eslint.org/

  4. 基本已替代以前的 JSLint

ESLint 提供以下支持

  1. ES : 泛指JavaScript

  2. JSX : JSX是一种JavaScript的语法扩展,运用于React架构中,其格式比较像是模版语言,但事实上完全是在JavaScript内部实现的。元素是构成React应用的最小单位,JSX就是用来声明React当中的元素,React使用JSX来描述用户界面。

  3. style 检查 : 代码风格检查

  4. 自定义错误和提示

ESLint 提供以下几种校验

  1. 语法错误校验

  2. 不重要或丢失的标点符号,如分号

  3. 没法运行到的代码块(使用过 WebStorm 的童鞋应该了解)

  4. 未被使用的参数提醒

  5. 确保样式的统一规则,如 sass 或者 less

  6. 检查变量的命名

规则的错误等级有三种

  1. 0:关闭规则。

  2. 1:打开规则,并且作为一个警告(信息打印黄色字体)

  3. 2:打开规则,并且作为一个错误(信息打印红色字体)

相关配置文件

.eslintrc.js : 全局规则配置文件

'rules': { 
    'no-new': 1 
}

// 相关介绍
https://blog.csdn.net/waiwai_color/article/details/79867513

在 js/vue 文件中修改局部规则

/* eslint-disable no-new */
new Vue({
  // 挂载在index.html的#app元素上
  el: '#app',
  components: {
    App
  },
  template: '<App/>'
})

.eslintignore: 指令检查忽略的文件

// 不建议忽略js,但是vue可以忽略
*.js 
*.vue

再次运行,不会再有报错

在这里插入图片描述

六、基于 vue-cli 使用组件

组件基本介绍

vue 文件的组成

1) 模板页面,注:Vue只允许模板里存在一个根节点。
	<template> 页面模板 </template> 
	
2) JS 模块对象 
	<script> 
		export default { 
            data() {
                return {}
            }, 
            methods: {}, 
            computed: {}, 
            components: {} 
        } 
	</script> 

3) 样式 
	<style>
        样式定义 
	</style>

基本使用

  1. 引入组件

  2. 映射成标签

  3. 使用组件标签

<template>
	<!-- 3、使用组件标签 -->
	<HelloWorld></HelloWorld> 
	<hello-world></hello-world> 
</template>

<script>
// 1、引入组件
import HelloWorld from "./components/HelloWorld";
export default {
  // 2、映射组件标签
  components: {
    HelloWorld,
  }
};
</script>

关于标签名与标签属性名书写问题

  1. 写法一: 一模一样

  2. 写法二: 大写变小写, 并用-连接

组件使用案例

页面效果图

在这里插入图片描述

页面静态代码

HTML代码

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <link rel="stylesheet" href="./bootstrap.css">
        <link rel="stylesheet" href="index.css">
    </head>
    <body>
        <div id="app">
            <div>
                <header class="site-header jumbotron">
                    <div class="container">
                        <div class="row">
                            <div class="col-xs-12">
                                <h1>请发表对Vue的评论</h1>
                            </div>
                        </div>
                    </div>
                </header>
                <div class="container">
                    <div class="col-md-4">
                        <form class="form-horizontal">
                            <div class="form-group">
                                <label>用户名</label>
                                <input type="text" class="form-control" placeholder="用户名">
                            </div>
                            <div class="form-group">
                                <label>评论内容</label>
                                <textarea class="form-control" rows="6" placeholder="评论内容"></textarea>
                            </div>
                            <div class="form-group">
                                <div class="col-sm-offset-2 col-sm-10">
                                    <button type="button" class="btn btn-default pull-right">提交</button>
                                </div>
                            </div>
                        </form>
                    </div>
                    <div class="col-md-8">
                        <h3 class="reply">评论回复:</h3>
                        <h2 style='display: none'>暂无评论,点击左侧添加评论!!!</h2>
                        <ul class="list-group">
                            <li class="list-group-item">
                                <div class="handle">
                                    <a href="javascript:;">删除</a>
                                </div>
                                <p class="user"><span >xxx</span><span>说:</span></p>
                                <p class="centence">Vue 不错!</p>
                            </li>
                            <li class="list-group-item">
                                <div class="handle">
                                    <a href="javascript:;">删除</a>
                                </div>
                                <p class="user"><span >yyy</span><span>说:</span></p>
                                <p class="centence">Vue 有点难!</p>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
        </div>
    </body>
</html>

css代码

.reply {
    margin-top: 0px;
}

li {
    transition: .5s;
    overflow: hidden;
}

.handle {
    width: 40px;
    border: 1px solid #ccc;
    background: #fff;
    position: absolute;
    right: 10px;
    top: 1px;
    text-align: center;
}

.handle a {
    display: block;
    text-decoration: none;
}

.list-group-item .centence {
    padding: 0px 50px;
}

.user {
    font-size: 22px;
}

Bootstrap样式文件不列举;静态页面代码在sources/静态页面/comment_page中;

组件化编码步骤

拆分组件

根据页面拆分出来如下组件:App组件、Add组件、List组件、Item组件;

在这里插入图片描述

静态组件

复制出来一份src代码,作为base代码以备后续使用;在src目录下面定义如下结构:

src
	|- components
		Add.vue
		Item.vue
		List.vue
	App.vue
	main.js

入口main.js文件基本不变,都是用来引入相关资源的;

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

/* eslint-disable no-new */
new Vue({
  el: '#app',
  components: {
    App
  },
  template: '<App/>'
})

bootstrap.css文件在每个组件里面都要使用,我们将其放在static/css目录下面,在根目录下面的index.html文件中引入;

<link rel="stylesheet" href="./static/css/bootstrap.css">

根组件App.vue内容,把静态页面中的静态代码复制到App.vue的template中;

​ 静态页面index.html里面复制;

在这里插入图片描述

​ App.vue组件里面粘贴;

在这里插入图片描述

新增组件Add.vue内容,把container第一部分内容复制到Add.vue的template中;

在这里插入图片描述

列表组件List.vue内容,把container第二部分内容复制到List.vue的template中;

在这里插入图片描述

列表项组件Item.vue内容,把列表项相关代码复制到Item.vue的template中;

在这里插入图片描述

把每个组件相关的样式放到各自的组件中;

  • List.vue样式:
.reply {
  margin-top: 0px;
}
  • Item.vue样式:
li {
  transition: .5s;
  overflow: hidden;
}

.handle {
  width: 40px;
  border: 1px solid #ccc;
  background: #fff;
  position: absolute;
  right: 10px;
  top: 1px;
  text-align: center;
}

.handle a {
  display: block;
  text-decoration: none;
}

.list-group-item .centence {
  padding: 0px 50px;
}

.user {
  font-size: 22px;
}

组件引入

​ 在App组件中引入Add和List;在List中引入Item;

<div class="container">
    <Add/>
    <List/>
</div>
<script>
// App组件中引入Add和List
import Add from "./components/Add";
import List from "./components/List";

export default {
  // 映射组件标签
  components: {
    Add,
    List,
  },
};
</script>
<ul class="list-group">
    <Item />
</ul>

<script>
    // 在List组件引入Item
    import Item from "./Item";

    export default {
        comments: {
            Item,
        },
    };
</script>
动态组件

结合Model数据实现;那么数据放在那个组件中?既然Add和List都会用到这个数据,所以我们将其放在App父组件中,那么子组件都可以使用;

在App组件中定义数据,并且通过属性绑定向子组件传递数据:

<div class="container">
    <Add />
    <List :comments="comments" />
</div>

export default {
  data() {
    return {
      // 公用的数据
      comments: [
        {
          name: "张三",
          content: "Vue 真好用",
        },
        {
          name: "李四",
          content: "Vue 真难啊",
        },
      ],
    };
  },
  // 映射组件标签
  components: {
    Add,
    List,
  },
};
</script>

在List组件中接受父组件传递过来的数据;

<ul class="list-group">
    <!-- 遍历comments,并且把每个comments的数据传递给子组件Item  -->
    <Item v-for="(comment, index) in comments" :key="index" :comment="comment"/>
</ul>

<script>
// 在List组件引入Item
import Item from "./Item";

export default {
  props: ["comments"],
  components: {
    Item,
  },
};
</script>

在Item组件中接收父组件传递过来的数据;

<template>
  <li class="list-group-item">
    <div class="handle">
      <a href="javascript:;">删除</a>
    </div>
    <p class="user">
      <span>{{ comment.name }}</span
      ><span>说:</span>
    </p>
    <p class="centence">{{ comment.content }}</p>
  </li>
</template>

<script>
export default {
  props: ["comment"],
};
</script>

数据动态显示啦;

在这里插入图片描述

交互式添加

数据在哪个组件,更新数据的行为就应该定义在哪个组件;(重点)

Add组件编码

1、给提交按钮添加事件,在事件里面获取表单数据;

2、对表单数据进行校验

3、把数据传递给父组件

4、清空表单数据

<template>
  <div class="col-md-4">
    <form class="form-horizontal">
      <div class="form-group">
        <label>用户名</label>
        <!-- 双向数据绑定 -->
        <input
          type="text"
          class="form-control"
          placeholder="用户名"
          v-model="name"
        />
      </div>
      <div class="form-group">
        <label>评论内容</label>
        <!-- 双向数据绑定 -->
        <textarea
          class="form-control"
          rows="6"
          placeholder="评论内容"
          v-model="content"
        ></textarea>
      </div>
      <div class="form-group">
        <div class="col-sm-offset-2 col-sm-10">
          <!-- 绑定事件 -->
          <button type="button" class="btn btn-default pull-right" @click="add">
            提交
          </button>
        </div>
      </div>
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      name: "",
      content: "",
    };
  },
  props: ["addComment"],
  methods: {
    add() {
      // 1.检查输入的合法性
      const { name, content } = this;
      // 姓名校验
      let name_reg = /^[\u4e00-\u9fa5]{2,4}$/;
      if (!name_reg.test(name)) {
        alert("请输入2-4位中文姓名");
        return;
      }
      if (content.length == "") {
        alert("评论不能为空");
        return;
      }

      // 2.根据输入的数据封装成一个comment对象
      const comment = {
        name,
        content,
      };

      // 3.添加到comments中去,子向父传递数据
      this.addComment(comment);

      // 4.清空数据
      this.name = "";
      this.content = "";
    },
  },
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
App组件编码

接收子组件传递的数据,然后添加到comments数组中

<div class="container">
    <Add :addComment="addComment"/>
    <List :comments="comments" />
</div>
<script>
// App组件中引入Add和List
import Add from "./components/Add";
import List from "./components/List";

export default {
  data() {
    return {
      // 公用的数据
      comments: [
        {
          name: "张三",
          content: "Vue 真好用",
        },
        {
          name: "李四",
          content: "Vue 真难啊",
        },
      ],
    };
  },
  methods: {
    addComment(comment){
      this.comments.unshift(comment);
    }
  },
  // 映射组件标签
  components: {
    Add,
    List,
  },
};
</script>

交互式删除

Item组件编码

给删除链接添加事件,当点击删除按钮的时候,触发事件删除当前数据;根据下标删除;

<a href="javascript:;" @click="deleteItem">删除</a>

<script>
export default {
  props: ["comment", "index", "deleteComment"],
  methods: {
    deleteItem() {
      const { comment } = this;
      let tf = window.confirm(`确定删除${comment.name}的评论吗?`);
      if (tf) {
        // deleteComment是在App组件定义,逐层传递下来
        // index是List父组件传递过来的
        this.deleteComment(this.index);
      }
    },
  },
};
</script>
App组件编码

定义根据下标删除评论的方法deleteComment,逐层传递给Item

 <List :comments="comments" :deleteComment="deleteComment" />

<script>
// App组件中引入Add和List
import Add from "./components/Add";
import List from "./components/List";

export default {
  data() {
    return {
      // 公用的数据
      comments: [
        {
          name: "张三",
          content: "Vue 真好用",
        },
        {
          name: "李四",
          content: "Vue 真难啊",
        },
      ],
    };
  },
  methods: {
    addComment(comment) {
      this.comments.unshift(comment);
    },
    deleteComment(index) {
      this.comments.splice(index, 1);
    },
  },
  // 映射组件标签
  components: {
    Add,
    List,
  },
};
</script>
List组件编码

接收App传递过来的deleteComment,继续传递给Item,并且传递index下标给Item

a<h2 v-show="comments.length == 0">暂无评论,点击左侧添加评论!!!</h2>
<ul class="list-group">
    <!-- 遍历comments,并且把每个comments的数据传递给子组件Item  -->
    <Item
          v-for="(comment, index) in comments"
          :key="index"
          :comment="comment"
          :deleteComment="deleteComment"
          :index="index"
          />
</ul>
<script>
// 在List组件引入Item
import Item from "./Item";

export default {
  props: ["comments", "deleteComment"],
  components: {
    Item,
  },
};
</script>

组件间通信

组件间通信基本原则

  1. 不要在子组件中直接修改父组件的状态数据

  2. 数据在哪, 更新数据的行为(函数)就应该定义在哪

vue 组件间通信方式

  1. props

  2. vue 的自定义事件

  3. vuex(后面单独讲)

组件间通信

方式一:props
使用组件标签时
<!-- 父组件向子组件传值 -->
<my-component name='tom' :age='3' :set-name='setName'></my-component>
定义 MyComponent 时
  1. 在组件内声明所有的 props

  2. 方式一: 只指定名称

props: ['name', 'age', 'setName']
  1. 方式二: 指定名称和类型
props: { 
    name: String, 
    age: Number, 
    setNmae: Function 
}
  1. 方式三: 指定名称/类型/必要性/默认值
props: { 
    name: {
        type: String, 
        required: true, 
        default:xxx
    }, 
}
注意
1) 此方式用于父组件向子组件传递数据 
2) 所有标签属性都会成为组件对象的属性, 模板页面可以直接引用 
3) 问题: 
	a. 如果需要向非子后代传递数据必须多层逐层传递 
	b. 兄弟组件间也不能直接 props 通信, 必须借助父组件才可以
方式二:vue 自定义事件
绑定事件监听
// 方式一: 通过 v-on 绑定
 @delete_todo="deleteTodo" 

// 方式二: 通过$on()
this.$refs.xxx.$on('delete_todo', function (todo) { 
    this.deleteTodo(todo) 
})
触发事件
// 触发事件(只能在父组件中接收) 
this.$emit(eventName, data)
注意
  1. 此方式只用于子组件向父组件发送消息(数据)

  2. 问题: 隔代组件或兄弟组件间通信此种方式不合适

七、前后端分离架构

背景

前后端分离已成为互联网项目开发的业界标准使用方式,通过 nginx + tomcat 的方式(也可以中间加一个 nodejs )有效的进行解耦,并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务(多种客户端,例如:浏览器,车载终端,安卓,IOS等等)打下坚实的基础。这个步骤是系统架构从猿进化成人的必经之路。

核心思想是前端 HTML 页面通过 AJAX 调用后端的 RESTFUL API 接口并使用 JSON 数据进行交互。

  • Web服务器:一般指像 Nginx、Apache 这类的服务器,他们一般只能解析静态资源
  • 应用服务器:一般指像 Tomcat、Jetty、Undertow 这类的服务器可以解析动态资源也可以解析静态资源,但解析静态资源的能力没有 web 服务器好

一般都是只有 web 服务器才能被外网访问,应用服务器只能内网访问。

以前的 Java Web 项目大多数都是 Java 程序员又当爹又当妈,又搞前端,又搞后端。随着时代的发展,渐渐的许多大中小公司开始把前后端的界限分的越来越明确,前端工程师只管前端的事情,后端工程师只管后端的事情。正所谓术业有专攻,一个人如果什么都会,那么他可能什么都不精。大中型公司需要专业人才,小公司需要全才,但是对于个人职业发展来说,前后端需要分离。

未分离时代

早期主要使用MVC框架,Jsp+Servlet的结构图如下:

在这里插入图片描述

就是所有的请求都被发送给作为控制器的 Servlet,它接受请求,并根据请求信息将它们分发给适当的 JSP 来响应。同时,Servlet 还根据 JSP 的需求生成 JavaBean 的实例并输出给 JSP 环境。JSP 可以通过直接调用方法或使用 UseBean 的自定义标签得到 JavaBean 中的数据。需要说明的是,这个 View 还可以采用 Velocity、Freemaker 等模板引擎。使用了这些模板引擎,可以使得开发过程中的人员分工更加明确,还能提高开发效率。

那么,在这个时期,开发方式有如下:

在这里插入图片描述

其实很多小型传统软件公司至今还在使用此种开发方式。那么这种方式的缺点总结为以下几点:

1、前端无法单独调试,开发效率低

2、前端不可避免会遇到后台代码,例如:

<body>
   <%
       request.setCharacterEncoding("utf-8")
       String name=request.getParameter("username");
       out.print(name);
   %>
</body>

3、JSP 本身所导致的一些其他问题。比如,JSP 第一次运行的时候比较缓慢,因为其中包含一个将 JSP 翻译为 Servlet 的步骤。再比如,因为同步加载的原因,在 JSP 中有很多内容的情况下,页面响应会很慢。

这种方式耦合性太强。那么,就算你用了 freemarker 等模板引擎,不用写 Java 代码。那前端也不可避免的要去重新学习该模板引擎的模板语法,无谓增加了前端的学习成本。正如我们后端开发不想写前端一样,你想想如果你的后台代码里嵌入前端代码,你是什么感受?因此,这种方式十分不妥。

半分离时代

前后端半分离,前端负责开发页面,通过接口 Ajax 获取数据,采用 Dom 操作对页面进行数据绑定,最终是由前端把页面渲染出来。这也就是 Ajax 与 SPA 应用(单页应用)结合的方式,其结构图如下:

在这里插入图片描述

步骤如下:
1、浏览器请求,CDN 返回 HTML 页面
2、HTML 中的 JS 代码以 Ajax 方式请求后台的 Restful 接口
3、接口返回 Json 数据,页面解析 Json 数据,通过 Dom 操作渲染页面

后端提供的都是以 JSON 为数据格式的 API 接口供前端使用,同样提供给后端 Server 的也是 JSON 格式的 API 接口。

那么意味着整个 WEB 工作流程是:

1、打开 web,加载基本资源,如 CSS,JS 等
2、发起一个 Ajax 请求到服务端获取数据,同时展示 loading
3、得到 Json 格式的数据后,再根据逻辑选择模板渲染出 DOM 字符串
4、将 DOM 字符串插入页面中 web view 渲染出 DOM 结构

这些步骤都由用户所使用的设备中逐步执行,也就是说用户的设备性能与 APP 的运行速度联系的更紧换句话说就是如果用户的设备很低端,那么 APP 打开页面的速度会越慢。

为什么说是半分离的?因为不是所有页面都是单页面应用,在多页面应用的情况下,前端因为没有掌握 Controller 层,前端需要跟后端讨论,某个页面是要同步输出,还是异步 Json 渲染。而且,即使在这一时期,通常也是一个工程师搞定前后端所有工作。因此,在这一阶段,只能算半分离。

首先,这种方式的优点是很明显的。前端不会嵌入任何后台代码,前端专注于 HTML、CSS、JS 的开发,不依赖于后端。自己还能够模拟Json 数据来渲染页面。发现 Bug,也能迅速定位出是谁的问题。

然而,在这种架构下,还是存在明显的弊端的。最明显的有如下几点:

1、JS 存在大量冗余,在业务复杂的情况下,页面的渲染部分的代码,非常复杂
2、在 Json 返回的数据量比较大的情况下,渲染的十分缓慢,会出现页面卡顿的情况
3、SEO(搜索引擎优化)非常不方便,由于搜索引擎的爬虫无法爬下 JS 异步渲染的数据,导致这样的页面,SEO 会存在一定的问题
4、资源消耗严重,在业务复杂的情况下,一个页面可能要发起多次 HTTP 请求才能将页面渲染完毕

正是因为如上缺点,我们才需要真正的前后端分离架构。

分离时代

在前后端完全分离这一时期,前端的范围被扩展,拥有自己的 MVC 架构。

  • View 层:为用户提供视觉呈现并与用户产生交互的 HTML 片段
  • Model 层:数据以及数据的操作和行为,数据通常是由后端服务提供的 Json
  • Controller 层:业务逻辑层,协调视图层和数据层

Controller 层如何实现呢?就是使用 node.js。
node.js 适合运用在高并发、I/O 密集、少量业务逻辑的场景。最重要的一点是,前端不用再学一门其他的语言了,对前端来说,上手度大大提高。架构图如下:

在这里插入图片描述

node.js 的作用在 MVC 中相当于 C(控制器),用 node.js 来作为桥梁架接后端服务提供的 JSON 数据。

浏览器不再直接请求后台服务的 API,而是:

1、浏览器请求 node.js 服务
2、由 node.js 再发起 HTTP 去请求 Java 后台
3、Java 后台依然原样输出 JSON 给 node.js
4、node.js 收到 JSON 后再渲染出 HTML 页面
5、node.js 直接将 HTML 页面 flush 到浏览器

这样,浏览器得到的就是普通的 HTML 页面,而不用再发 Ajax去 请求服务器了。

淘宝的前端团队提出的中途岛(Midway Framework)的架构如下图所示:

在这里插入图片描述

增加node.js作为中间层,具体有哪些好处呢?

1、适配性提升
其实在开发过程中,经常会给 PC、mobile、app 端各自研发一套前端。其实对于这三端来说,大部分端业务逻辑是一样的。唯一区别就是交互展现逻辑不同。后端为了这些不同端页面展示逻辑,模版无法重用,徒增和前端沟通端成本。如果增加了 node.js 层,此时架构图如下:

在这里插入图片描述

在该结构下,每种前端的界面展示逻辑由 node.js 维护。如果产品经理中途想要改动界面什么的,可以由前端自己专职维护,后端无需操心。前后端各司其职,后端专注自己的业务逻辑开发,前端专注产品效果开发。

2、响应速度提升
有时候会遇到后端返回给前端的数据太简单了,前端需要对这些数据进行逻辑运算。那么在数据量比较小的时候,对其做运算分组等操作,并无影响。但是当数据量大的时候,会有明显的卡顿效果。这时候,node.js 中间层其实可以将很多这样的代码放入 node.js 层处理、也可以替后端分担一些简单的逻辑、又可以用模板引擎自己掌握前台的输出。这样做灵活度、响应度都大大提升。

3、性能得到提升
就是单一职责原则。从该角度来看,请求一个页面,可能要响应很多个后端接口,请求变多了,自然速度就变慢了,这种现象在 mobile 端更加严重。采用 node.js 作为中间层,将页面所需要的多个后端数据,直接在内网阶段就拼装好,再统一返回给前端,会得到更好的性能。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JTZ001

你的鼓励是我创作的最大动力?

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值